@torrent-tv/proxy 2.107.46 → 2.107.47
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/package.json
CHANGED
|
@@ -77,17 +77,15 @@ export async function handleApiSourceFilesGet(req, reply, { sourceRegistry, torr
|
|
|
77
77
|
const generation = generationRaw !== "" && /^\d+$/.test(generationRaw) ? Number(generationRaw) : null;
|
|
78
78
|
const linkMbps = Number(req.query?.linkMbps);
|
|
79
79
|
const linkSampleAgeMs = Number(req.query?.linkSampleAgeMs);
|
|
80
|
-
const waitElapsedMs = Number(req.query?.waitElapsedMs);
|
|
81
80
|
const earlyLink = Number.isFinite(linkMbps) && linkMbps > 0
|
|
82
81
|
? { linkMbps, linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null }
|
|
83
82
|
: {};
|
|
84
|
-
const earlyWaitAge = Number.isFinite(waitElapsedMs) && waitElapsedMs >= 0 ? { waitElapsedMs } : {};
|
|
85
83
|
const files = contents.files().map(file => {
|
|
86
84
|
if (!consumerId || !Number.isSafeInteger(generation) || file.kind !== "video" || file.excludedReason) return file;
|
|
87
|
-
|
|
85
|
+
viewerRequests?.noteWaitFileSnapshot?.({ consumerId, generation, sourceKey,
|
|
88
86
|
fileIndex: file.fileIndex, fileName: file.relativePath ?? file.name, fileLength: file.length,
|
|
89
|
-
...earlyLink
|
|
90
|
-
return
|
|
87
|
+
...earlyLink });
|
|
88
|
+
return file;
|
|
91
89
|
});
|
|
92
90
|
return reply.send({
|
|
93
91
|
name: torrent.name ?? "",
|
|
@@ -40,15 +40,15 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
40
40
|
? Number(req.query.fileLength) : null;
|
|
41
41
|
const linkMbps = Number(req.query.linkMbps);
|
|
42
42
|
const linkSampleAgeMs = Number(req.query.linkSampleAgeMs);
|
|
43
|
-
const waitElapsedMs = Number(req.query.waitElapsedMs);
|
|
44
43
|
const earlyLink = Number.isFinite(linkMbps) && linkMbps > 0
|
|
45
44
|
? { linkMbps, linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null }
|
|
46
45
|
: {};
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
const noteInitialSnapshot = () => {
|
|
47
|
+
if (consumerId && Number.isSafeInteger(generation) && Number.isSafeInteger(fileIndex)) {
|
|
48
|
+
viewerRequests?.noteWaitFileSnapshot?.({ consumerId, generation, sourceKey, fileIndex,
|
|
49
|
+
fileName: requestedFileName, fileLength: requestedFileLength, ...earlyLink });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
52
|
|
|
53
53
|
let torrent;
|
|
54
54
|
try {
|
|
@@ -59,8 +59,8 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
59
59
|
return reply.code(500).send({ error: `Failed to load torrent: ${message}` });
|
|
60
60
|
}
|
|
61
61
|
if (!torrent) {
|
|
62
|
-
|
|
63
|
-
return reply.code(202).send({ pending: true
|
|
62
|
+
noteInitialSnapshot();
|
|
63
|
+
return reply.code(202).send({ pending: true });
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Awaited: with the torrent on its own thread this is a round trip, not a
|
|
@@ -74,8 +74,8 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
74
74
|
logger.info(`[stats] ${sourceKey.slice(0, 8)} getFileStatsMs=${(performance.now() - statsStartedAt).toFixed(1)}`);
|
|
75
75
|
}
|
|
76
76
|
if (stats?.pending || !stats) {
|
|
77
|
-
|
|
78
|
-
return reply.code(202).send({ pending: true
|
|
77
|
+
noteInitialSnapshot();
|
|
78
|
+
return reply.code(202).send({ pending: true });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// Diagnostic: surface the real swarm state per poll so a cold-start download
|
|
@@ -149,10 +149,10 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
|
|
|
149
149
|
`file=${filePct} header=${header}${detail}`
|
|
150
150
|
);
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
if (consumerId && Number.isSafeInteger(generation)) {
|
|
153
|
+
viewerRequests?.noteWaitSourceSnapshot?.({ consumerId, generation, sourceKey, fileIndex,
|
|
154
154
|
fileName: torrent.files?.[fileIndex]?.name ?? requestedFileName, fileLength: requestedFileLength,
|
|
155
|
-
stats, ...earlyLink
|
|
156
|
-
|
|
157
|
-
return reply.send(
|
|
155
|
+
stats, ...earlyLink });
|
|
156
|
+
}
|
|
157
|
+
return reply.send(stats);
|
|
158
158
|
}
|
|
@@ -91,8 +91,8 @@ export class ViewerRequests {
|
|
|
91
91
|
#host;
|
|
92
92
|
/** @type {WeakMap<object, Map<string, object>>} */
|
|
93
93
|
#rateStateBySession = new WeakMap();
|
|
94
|
-
/** @type {Map<string, { consumerId: string, generation: number, fileIndex: number | null, waitId: string, features: object, updatedAt: number,
|
|
95
|
-
#
|
|
94
|
+
/** @type {Map<string, { consumerId: string, generation: number, fileIndex: number | null, waitId: string, features: object, updatedAt: number, observedWaits: Set<number> }>} */
|
|
95
|
+
#waitObservations = new Map();
|
|
96
96
|
/**
|
|
97
97
|
* What the start forecasts cost, said once a minute: how many there were,
|
|
98
98
|
* their time on the main thread, and for each track whether the times of its
|
|
@@ -130,17 +130,10 @@ export class ViewerRequests {
|
|
|
130
130
|
this.#host = host;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
* a file, so the original action need not wait for a torrent-worker round trip.
|
|
136
|
-
*
|
|
137
|
-
* @param {{ consumerId: string, generation: number, sourceKey: string, fileIndex: number, fileName: string, fileLength: number }} params
|
|
138
|
-
* @returns {object | null}
|
|
139
|
-
*/
|
|
140
|
-
waitEstimateForFile({ consumerId, generation, sourceKey, fileIndex, fileName, fileLength, linkMbps, linkSampleAgeMs,
|
|
141
|
-
waitElapsedMs }) {
|
|
133
|
+
/** Record the facts known while listing a source, without producing an ETA. */
|
|
134
|
+
noteWaitFileSnapshot({ consumerId, generation, sourceKey, fileIndex, fileName, fileLength, linkMbps, linkSampleAgeMs }) {
|
|
142
135
|
if (!consumerId || !Number.isSafeInteger(generation) || typeof sourceKey !== "string" ||
|
|
143
|
-
!Number.isSafeInteger(fileIndex) || fileIndex < 0) return
|
|
136
|
+
!Number.isSafeInteger(fileIndex) || fileIndex < 0) return;
|
|
144
137
|
const waitId = `${sourceKey}:${fileIndex}:${generation}`;
|
|
145
138
|
const key = `${consumerId}:${waitId}`;
|
|
146
139
|
const extension = typeof fileName === "string" ? fileName.split(".").at(-1)?.toLowerCase() ?? "" : "";
|
|
@@ -156,48 +149,23 @@ export class ViewerRequests {
|
|
|
156
149
|
headerRemaining: null,
|
|
157
150
|
peers: null
|
|
158
151
|
};
|
|
159
|
-
const current = this.#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const changed = this.#recordEarlyWaitSnapshot(context, context.features);
|
|
164
|
-
if (changed) context.revision += 1;
|
|
165
|
-
this.#pruneEarlyWaits();
|
|
166
|
-
const elapsedMs = waitElapsedSince(context, waitElapsedMs);
|
|
167
|
-
const totalSeconds = context.activeTotalSeconds;
|
|
168
|
-
const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
|
|
169
|
-
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
170
|
-
this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds,
|
|
171
|
-
firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds });
|
|
172
|
-
return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
|
|
173
|
-
}
|
|
174
|
-
const sameWait = current?.waitId === waitId;
|
|
175
|
-
const revision = sameWait ? current.revision : 0;
|
|
176
|
-
const context = this.#setEarlyWait({ consumerId, generation, sourceKey, fileIndex, waitId, features, revision, current,
|
|
177
|
-
waitElapsedMs });
|
|
178
|
-
const changed = this.#recordEarlyWaitSnapshot(context, context.features);
|
|
179
|
-
if (sameWait && changed) context.revision += 1;
|
|
180
|
-
this.#pruneEarlyWaits();
|
|
181
|
-
const elapsedMs = waitElapsedSince(context, waitElapsedMs);
|
|
182
|
-
const totalSeconds = context.activeTotalSeconds;
|
|
183
|
-
const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
|
|
184
|
-
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
185
|
-
this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds,
|
|
186
|
-
firstRequiredSegmentsSeconds: context.activeFirstRequiredSegmentsSeconds,
|
|
187
|
-
firstRequiredStages: context.activeFirstRequiredStages });
|
|
188
|
-
return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
|
|
152
|
+
const current = this.#waitObservations.get(key);
|
|
153
|
+
const context = this.#setWaitObservation({ consumerId, generation, sourceKey, fileIndex, waitId, features, current });
|
|
154
|
+
this.#recordWaitSnapshot(context);
|
|
155
|
+
this.#pruneWaitObservations();
|
|
189
156
|
}
|
|
190
157
|
|
|
191
158
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
159
|
+
* Record current source and link facts for later forecast evaluation. These
|
|
160
|
+
* partial inputs do not determine the first presented frame, so this method
|
|
161
|
+
* does not produce a wait estimate.
|
|
194
162
|
*
|
|
195
163
|
* @param {{ consumerId: string, generation: number, sourceKey: string, fileIndex: number | null, fileName?: string, fileLength?: number | null, stats?: object }} params
|
|
196
164
|
* @returns {object | null}
|
|
197
165
|
*/
|
|
198
|
-
|
|
199
|
-
stats = {}, linkMbps, linkSampleAgeMs,
|
|
200
|
-
if (!consumerId || !Number.isSafeInteger(generation) || typeof sourceKey !== "string") return
|
|
166
|
+
noteWaitSourceSnapshot({ consumerId, generation, sourceKey, fileIndex, fileName = "", fileLength = null,
|
|
167
|
+
stats = {}, linkMbps, linkSampleAgeMs, processing = {} }) {
|
|
168
|
+
if (!consumerId || !Number.isSafeInteger(generation) || typeof sourceKey !== "string") return;
|
|
201
169
|
const waitId = `${sourceKey}:${fileIndex ?? "all"}:${generation}`;
|
|
202
170
|
const key = `${consumerId}:${waitId}`;
|
|
203
171
|
const extension = typeof fileName === "string" ? fileName.split(".").at(-1)?.toLowerCase() ?? "" : "";
|
|
@@ -220,68 +188,10 @@ export class ViewerRequests {
|
|
|
220
188
|
videoSpeedX: processing.videoSpeedX ?? null,
|
|
221
189
|
audioSpeedX: processing.audioSpeedX ?? null
|
|
222
190
|
};
|
|
223
|
-
const current = this.#
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
sourceObserved: true, waitElapsedMs });
|
|
228
|
-
const featuresChanged = this.#recordEarlyWaitSnapshot(context, context.features);
|
|
229
|
-
const elapsedMs = waitElapsedSince(context, waitElapsedMs);
|
|
230
|
-
// The wait ends when the start forecast lets the picture start, and the
|
|
231
|
-
// first required pieces being there is only part of that: a source
|
|
232
|
-
// supplied slower than the film plays is held until the cushion carries
|
|
233
|
-
// it to the end. Counted to the first pieces alone, the estimate reached
|
|
234
|
-
// zero with seven minutes still to wait, and the page had no time to show
|
|
235
|
-
// (Home Assistant 2026-10-26, Despicable Me 3 BDRemux at 0.53x,
|
|
236
|
-
// torrent-tv/meta#239).
|
|
237
|
-
const firstRequiredSeconds = processing.firstRequiredSegmentsSeconds;
|
|
238
|
-
const startGateSeconds = processing.startGateSeconds;
|
|
239
|
-
const measuredWorkSeconds = Number.isFinite(startGateSeconds) && startGateSeconds >= 0
|
|
240
|
-
? Math.max(startGateSeconds, Number.isFinite(firstRequiredSeconds) ? firstRequiredSeconds : 0)
|
|
241
|
-
: firstRequiredSeconds;
|
|
242
|
-
const hasActiveEstimate = Number.isFinite(measuredWorkSeconds) && measuredWorkSeconds >= 0;
|
|
243
|
-
const stagesChanged = Array.isArray(processing.firstRequiredStages) &&
|
|
244
|
-
JSON.stringify(processing.firstRequiredStages) !== JSON.stringify(context.activeFirstRequiredStages);
|
|
245
|
-
const activeTotalSeconds = hasActiveEstimate
|
|
246
|
-
? elapsedMs / 1000 + measuredWorkSeconds
|
|
247
|
-
: context.activeTotalSeconds;
|
|
248
|
-
const activeEstimateChanged = hasActiveEstimate &&
|
|
249
|
-
(activeTotalSeconds !== context.activeTotalSeconds || stagesChanged);
|
|
250
|
-
if (hasActiveEstimate) {
|
|
251
|
-
context.activeTotalSeconds = activeTotalSeconds;
|
|
252
|
-
context.activeFirstRequiredSegmentsSeconds = measuredWorkSeconds;
|
|
253
|
-
if (Array.isArray(processing.firstRequiredStages)) {
|
|
254
|
-
context.activeFirstRequiredStages = processing.firstRequiredStages;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const changed = featuresChanged || activeEstimateChanged;
|
|
258
|
-
if (sameWait && changed) context.revision += 1;
|
|
259
|
-
this.#pruneEarlyWaits();
|
|
260
|
-
const totalSeconds = activeTotalSeconds;
|
|
261
|
-
const seconds = earlyWaitRemainingSeconds(totalSeconds, elapsedMs);
|
|
262
|
-
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
263
|
-
this.#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds,
|
|
264
|
-
firstRequiredSegmentsSeconds: hasActiveEstimate ? firstRequiredSeconds ?? null : context.activeFirstRequiredSegmentsSeconds,
|
|
265
|
-
startGateSeconds: hasActiveEstimate && Number.isFinite(startGateSeconds) ? startGateSeconds : null,
|
|
266
|
-
firstRequiredStages: context.activeFirstRequiredStages });
|
|
267
|
-
return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
#logEarlyForecast(context, { elapsedMs, seconds, totalSeconds, firstRequiredSegmentsSeconds,
|
|
271
|
-
startGateSeconds = null, firstRequiredStages }) {
|
|
272
|
-
if (context.loggedForecastRevision === context.revision) return;
|
|
273
|
-
const gated = Number.isFinite(startGateSeconds) &&
|
|
274
|
-
!(Number.isFinite(firstRequiredSegmentsSeconds) && firstRequiredSegmentsSeconds >= startGateSeconds);
|
|
275
|
-
const basis = gated ? "start-gate" : "first-required";
|
|
276
|
-
const estimate = { firstRequiredSegmentsSeconds, ...(Number.isFinite(startGateSeconds) ? { startGateSeconds } : {}),
|
|
277
|
-
...(Array.isArray(firstRequiredStages) ? { firstRequiredStages } : {}) };
|
|
278
|
-
logger.info(`early wait forecast ${JSON.stringify({ waitId: context.waitId, consumer: context.consumerId,
|
|
279
|
-
generation: context.generation, features: context.features, elapsedMs, revision: context.revision,
|
|
280
|
-
predicted: { remainingSeconds: seconds, totalSeconds, basis, ...estimate } })}`);
|
|
281
|
-
this.#host.observeWait?.(context.sourceKey, "process", { process: "P15", phase: "event",
|
|
282
|
-
waitId: context.waitId, consumerId: context.consumerId, generation: context.generation,
|
|
283
|
-
elapsedMs, revision: context.revision, remainingSeconds: seconds, totalSeconds, basis, ...estimate });
|
|
284
|
-
context.loggedForecastRevision = context.revision;
|
|
191
|
+
const current = this.#waitObservations.get(key);
|
|
192
|
+
const context = this.#setWaitObservation({ consumerId, generation, sourceKey, fileIndex, waitId, features, current });
|
|
193
|
+
this.#recordWaitSnapshot(context);
|
|
194
|
+
this.#pruneWaitObservations();
|
|
285
195
|
}
|
|
286
196
|
|
|
287
197
|
/**
|
|
@@ -296,20 +206,19 @@ export class ViewerRequests {
|
|
|
296
206
|
*/
|
|
297
207
|
noteFirstFrameObservation({ consumerId, generation, waitId, wait, elapsedMs, stages = null }) {
|
|
298
208
|
const key = `${consumerId}:${waitId}`;
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
209
|
+
const observation = this.#waitObservations.get(key);
|
|
210
|
+
if (!observation || observation.generation !== generation || !Number.isSafeInteger(wait) || wait < 0 || observation.observedWaits.has(wait) ||
|
|
301
211
|
!Number.isFinite(elapsedMs) || elapsedMs < 0) return;
|
|
302
|
-
const initialFeatures =
|
|
212
|
+
const initialFeatures = observation.snapshots[0];
|
|
303
213
|
if (initialFeatures) {
|
|
304
214
|
this.#host.noteFirstFrame?.({ seconds: elapsedMs / 1000, features: initialFeatures,
|
|
305
|
-
featureSnapshots:
|
|
215
|
+
featureSnapshots: observation.snapshots.slice(1), stages });
|
|
306
216
|
}
|
|
307
|
-
|
|
308
|
-
|
|
217
|
+
observation.observedWaits.add(wait);
|
|
218
|
+
observation.updatedAt = Date.now();
|
|
309
219
|
}
|
|
310
220
|
|
|
311
|
-
#
|
|
312
|
-
sourceObserved = false, waitElapsedMs }) {
|
|
221
|
+
#setWaitObservation({ consumerId, generation, sourceKey, fileIndex, waitId, features, current }) {
|
|
313
222
|
const sameWait = current?.waitId === waitId;
|
|
314
223
|
const context = sameWait ? current : {
|
|
315
224
|
consumerId,
|
|
@@ -318,26 +227,21 @@ export class ViewerRequests {
|
|
|
318
227
|
fileIndex,
|
|
319
228
|
waitId,
|
|
320
229
|
snapshots: [],
|
|
321
|
-
hasSourceObservation: false,
|
|
322
230
|
observedWaits: new Set()
|
|
323
231
|
};
|
|
324
232
|
const measuredAt = performance.now();
|
|
325
|
-
const actionElapsedMs = Number.isFinite(waitElapsedMs) && waitElapsedMs >= 0 ? waitElapsedMs : null;
|
|
326
|
-
const startedAt = actionElapsedMs === null
|
|
327
|
-
? Number.isFinite(context.startedAt) ? context.startedAt : measuredAt
|
|
328
|
-
: measuredAt - actionElapsedMs;
|
|
329
233
|
const ageElapsedMs = Number.isFinite(context.featuresMeasuredAt)
|
|
330
234
|
? Math.max(0, measuredAt - context.featuresMeasuredAt)
|
|
331
235
|
: 0;
|
|
332
236
|
const mergedFeatures = mergeWaitFeatures(context.features, features, ageElapsedMs);
|
|
333
237
|
Object.assign(context, { consumerId, generation, sourceKey, fileIndex, waitId, features: mergedFeatures,
|
|
334
|
-
featuresMeasuredAt: measuredAt,
|
|
335
|
-
|
|
336
|
-
this.#earlyWaits.set(`${consumerId}:${waitId}`, context);
|
|
238
|
+
featuresMeasuredAt: measuredAt, updatedAt: Date.now() });
|
|
239
|
+
this.#waitObservations.set(`${consumerId}:${waitId}`, context);
|
|
337
240
|
return context;
|
|
338
241
|
}
|
|
339
242
|
|
|
340
|
-
#
|
|
243
|
+
#recordWaitSnapshot(context) {
|
|
244
|
+
const features = context.features;
|
|
341
245
|
const previous = context.snapshots.at(-1);
|
|
342
246
|
if (previous && JSON.stringify(previous) === JSON.stringify(features)) return false;
|
|
343
247
|
const snapshot = { ...features };
|
|
@@ -373,12 +277,12 @@ export class ViewerRequests {
|
|
|
373
277
|
return bytes;
|
|
374
278
|
}
|
|
375
279
|
|
|
376
|
-
#
|
|
280
|
+
#pruneWaitObservations() {
|
|
377
281
|
const cutoff = Date.now() - 30 * 60_000;
|
|
378
|
-
for (const [key, value] of this.#
|
|
379
|
-
if (value.updatedAt < cutoff) this.#
|
|
282
|
+
for (const [key, value] of this.#waitObservations) {
|
|
283
|
+
if (value.updatedAt < cutoff) this.#waitObservations.delete(key);
|
|
380
284
|
}
|
|
381
|
-
while (this.#
|
|
285
|
+
while (this.#waitObservations.size > 1000) this.#waitObservations.delete(this.#waitObservations.keys().next().value);
|
|
382
286
|
}
|
|
383
287
|
|
|
384
288
|
/**
|
|
@@ -674,13 +578,12 @@ export class ViewerRequests {
|
|
|
674
578
|
videoSpeedX: null,
|
|
675
579
|
audioSpeedX: null
|
|
676
580
|
};
|
|
677
|
-
// A seek is a new
|
|
678
|
-
// generation
|
|
679
|
-
|
|
680
|
-
this.#
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
this.#recordEarlyWaitSnapshot(context, features);
|
|
581
|
+
// A seek is a new observation group even when a caller repeats the same
|
|
582
|
+
// generation; its source facts must not join observations from the prior wait.
|
|
583
|
+
this.#waitObservations.delete(key);
|
|
584
|
+
const context = this.#setWaitObservation({ consumerId, generation, sourceKey: file.sourceKey,
|
|
585
|
+
fileIndex: file.fileIndex, waitId, features, current: null });
|
|
586
|
+
this.#recordWaitSnapshot(context);
|
|
684
587
|
}
|
|
685
588
|
|
|
686
589
|
/**
|
|
@@ -1109,11 +1012,11 @@ export class ViewerRequests {
|
|
|
1109
1012
|
? this.#host.generationOfRequest(consumerId) : null;
|
|
1110
1013
|
const earlyWaitId = Number.isSafeInteger(generation)
|
|
1111
1014
|
? `${session.file.sourceKey}:${session.file.fileIndex}:${generation}` : null;
|
|
1112
|
-
if (earlyWaitId && this.#
|
|
1015
|
+
if (earlyWaitId && this.#waitObservations.has(`${consumerId}:${earlyWaitId}`) && primarySourceStats) {
|
|
1113
1016
|
const linkMeasuredAt = Number(viewerReading?.linkMeasuredAt);
|
|
1114
1017
|
const videoOperation = forecast.operations.find(operation => operation.track === "video");
|
|
1115
1018
|
const audioOperation = forecast.operations.find(operation => operation.track === "audio");
|
|
1116
|
-
|
|
1019
|
+
this.noteWaitSourceSnapshot({ consumerId, generation,
|
|
1117
1020
|
sourceKey: session.file.sourceKey, fileIndex: session.file.fileIndex,
|
|
1118
1021
|
fileName: session.file.relativePath ?? session.file.name ?? "",
|
|
1119
1022
|
fileLength: primarySourceStats.fileLength, stats: primarySourceStats,
|
|
@@ -1125,14 +1028,12 @@ export class ViewerRequests {
|
|
|
1125
1028
|
videoSpeedX: videoOperation?.speed,
|
|
1126
1029
|
audioSpeedX: audioOperation?.speed,
|
|
1127
1030
|
firstRequiredSegmentsSeconds: forecast.firstRequiredSegmentsSeconds,
|
|
1128
|
-
// When the start forecast lets the picture start: its delay while
|
|
1129
|
-
// it holds the start, and nothing to add once it does not.
|
|
1130
1031
|
...(forecast.ready !== true && Number.isFinite(forecast.delaySeconds)
|
|
1131
1032
|
? { startGateSeconds: forecast.delaySeconds } : {}),
|
|
1132
1033
|
firstRequiredStages: forecast.firstRequiredStages
|
|
1133
1034
|
} });
|
|
1134
|
-
if (waitEstimate) forecast.waitEstimate = waitEstimate;
|
|
1135
1035
|
}
|
|
1036
|
+
forecast.waitEstimate = null;
|
|
1136
1037
|
const unavailableInput = forecast.unavailableSource;
|
|
1137
1038
|
const previousInputState = measurement.forecastInputState ?? null;
|
|
1138
1039
|
const currentInputState = forecastInputState(tracks, unavailableInput);
|
|
@@ -1466,31 +1367,6 @@ function linkMeasurementOf(viewer, reportedMbps, reportedAgeMs) {
|
|
|
1466
1367
|
return { mbps, ageMs };
|
|
1467
1368
|
}
|
|
1468
1369
|
|
|
1469
|
-
/**
|
|
1470
|
-
* Convert a learned full wait into the time still expected after this action.
|
|
1471
|
-
*
|
|
1472
|
-
* @param {number | null} totalSeconds
|
|
1473
|
-
* @param {number} elapsedMs
|
|
1474
|
-
* @returns {number | null}
|
|
1475
|
-
*/
|
|
1476
|
-
function earlyWaitRemainingSeconds(totalSeconds, elapsedMs) {
|
|
1477
|
-
if (!Number.isFinite(totalSeconds) || totalSeconds < 0 || !Number.isFinite(elapsedMs) || elapsedMs < 0) return null;
|
|
1478
|
-
return Math.max(0, totalSeconds - elapsedMs / 1000);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
/**
|
|
1482
|
-
* Prefer the browser's monotonic age of the original action; retain a proxy
|
|
1483
|
-
* monotonic clock fallback for older pages that do not send it.
|
|
1484
|
-
*
|
|
1485
|
-
* @param {object} context
|
|
1486
|
-
* @param {number | undefined} reportedElapsedMs
|
|
1487
|
-
* @returns {number}
|
|
1488
|
-
*/
|
|
1489
|
-
function waitElapsedSince(context, reportedElapsedMs) {
|
|
1490
|
-
if (Number.isFinite(reportedElapsedMs) && reportedElapsedMs >= 0) return reportedElapsedMs;
|
|
1491
|
-
return Number.isFinite(context.startedAt) ? Math.max(0, performance.now() - context.startedAt) : 0;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
1370
|
/**
|
|
1495
1371
|
* The download service a source's forecast runs on: what the swarm carried
|
|
1496
1372
|
* while our selection held none of its connections back, but never less than
|