@torrent-tv/proxy 2.101.29 → 2.101.30
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
|
@@ -91,14 +91,19 @@ export async function handleApiClientLogsPost(req, reply, { clientLogs, viewerRe
|
|
|
91
91
|
}
|
|
92
92
|
for (const row of rows) {
|
|
93
93
|
const message = row && typeof row.msg === "string" ? row.msg : "";
|
|
94
|
-
const match = /^\[first-frame-observation\] consumer=([^ ]+) generation=(\d+) waitId=([^ ]+) wait=(\d+) audioReady=(true|false) elapsedMs=([\d.]+)
|
|
94
|
+
const match = /^\[first-frame-observation\] consumer=([^ ]+) generation=(\d+) waitId=([^ ]+) wait=(\d+) audioReady=(true|false) elapsedMs=([\d.]+)(?: coldStartTotalMs=([\d.]+) transportMs=([\d.]+) planMs=([\d.]+) prepareMs=([\d.]+) prebufferMs=([\d.]+) presentationMs=([\d.]+))?$/.exec(message);
|
|
95
95
|
if (match) {
|
|
96
96
|
const elapsedMs = Number(match[6]);
|
|
97
97
|
if (match[1].length <= 64 && Number.isSafeInteger(Number(match[2])) && match[3].length <= 256 &&
|
|
98
98
|
Number.isSafeInteger(Number(match[4])) && match[5] === "true" &&
|
|
99
99
|
Number.isFinite(elapsedMs) && elapsedMs >= 0) {
|
|
100
|
+
const stageValues = match.slice(7).map(value => value === undefined ? null : Number(value));
|
|
101
|
+
const stages = stageValues.every(value => value !== null && Number.isFinite(value) && value >= 0)
|
|
102
|
+
? { totalMs: stageValues[0], transportMs: stageValues[1], planMs: stageValues[2],
|
|
103
|
+
prepareMs: stageValues[3], prebufferMs: stageValues[4], presentationMs: stageValues[5] }
|
|
104
|
+
: null;
|
|
100
105
|
viewerRequests?.noteFirstFrameObservation?.({ consumerId: match[1], generation: Number(match[2]),
|
|
101
|
-
waitId: match[3], wait: Number(match[4]), elapsedMs });
|
|
106
|
+
waitId: match[3], wait: Number(match[4]), elapsedMs, stages });
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* NOT A TABLE OF QUALITY, and not a condition for anything. The proxy selects,
|
|
7
7
|
* admits and serves with nothing kept here at all: the startup calibration is
|
|
8
8
|
* what qualifies a mode and prices it, and the admission is what decides
|
|
9
|
-
* whether there is room. What is kept here makes
|
|
9
|
+
* whether there is room. What is kept here makes four of those figures more
|
|
10
10
|
* exact on THIS machine once it has done the work for real:
|
|
11
11
|
*
|
|
12
12
|
* 1. how fast an encode of this mode, at this size, actually ran alone on this
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* 3. how long an output of this mode took to have the piece a viewer needed
|
|
20
20
|
* after it was asked for, and what that viewer held when they moved — used
|
|
21
21
|
* as the time to ready when deciding whether a draining buffer is a threat.
|
|
22
|
+
* 4. the measured transport, track-plan, output-preparation, prebuffer and
|
|
23
|
+
* first-presented-frame stages for an earlier start on this proxy
|
|
24
|
+
* configuration, used as separate inputs to an early wait forecast.
|
|
22
25
|
*
|
|
23
26
|
* WHAT IS NEVER KEPT: a picture, a clip, a segment, anything about a viewer
|
|
24
27
|
* beyond the seconds they held, and anything that leaves this machine.
|
|
@@ -265,31 +268,21 @@ export class LocalObservations {
|
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* only on the encoder configurations present on this host.
|
|
271
|
+
* The viewer presented its first audio-ready frame: keep the separately
|
|
272
|
+
* measured stage timings and the input snapshots from the start of that run.
|
|
271
273
|
*
|
|
272
|
-
* @param {{ seconds: number, features: object }} params
|
|
274
|
+
* @param {{ seconds: number, features: object, featureSnapshots?: object[], stages?: object | null }} params
|
|
273
275
|
* @returns {void}
|
|
274
276
|
*/
|
|
275
|
-
noteFirstFrame({ seconds, features }) {
|
|
277
|
+
noteFirstFrame({ seconds, features, featureSnapshots = null, stages = null }) {
|
|
276
278
|
if (!Number.isFinite(seconds) || seconds < 0 || !features || typeof features !== "object") return;
|
|
277
279
|
const key = this.#firstFrameKey();
|
|
280
|
+
const snapshots = [features, ...(Array.isArray(featureSnapshots) ? featureSnapshots : [])]
|
|
281
|
+
.filter(value => value && typeof value === "object").slice(0, 2).map(firstFrameFeatures);
|
|
278
282
|
const readings = [...(this.#entries.firstFrame[key] ?? []), {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
downloadSpeed: nonNegative(features.downloadSpeed),
|
|
283
|
-
linkMbps: nonNegative(features.linkMbps),
|
|
284
|
-
linkSampleAgeMs: nonNegative(features.linkSampleAgeMs),
|
|
285
|
-
preparedOutputBytes: nonNegative(features.preparedOutputBytes),
|
|
286
|
-
fileAvailable: typeof features.fileAvailable === "boolean" ? features.fileAvailable : null,
|
|
287
|
-
headerRemaining: nonNegative(features.headerRemaining),
|
|
288
|
-
peers: nonNegative(features.peers),
|
|
289
|
-
videoOperation: operationOf(features.videoOperation),
|
|
290
|
-
audioOperation: operationOf(features.audioOperation),
|
|
291
|
-
videoSpeedX: nonNegative(features.videoSpeedX),
|
|
292
|
-
audioSpeedX: nonNegative(features.audioSpeedX),
|
|
283
|
+
...snapshots[0],
|
|
284
|
+
featureSnapshots: snapshots,
|
|
285
|
+
stages: stagesOf(stages),
|
|
293
286
|
at: this.#now()
|
|
294
287
|
}].slice(-READINGS_KEPT);
|
|
295
288
|
this.#entries.firstFrame[key] = readings;
|
|
@@ -297,62 +290,86 @@ export class LocalObservations {
|
|
|
297
290
|
}
|
|
298
291
|
|
|
299
292
|
/**
|
|
300
|
-
* Predict
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
293
|
+
* Predict the first-frame wait as the sum of separately predicted cold-start
|
|
294
|
+
* stages. Each stage uses all compatible measurements, weighted by the
|
|
295
|
+
* normalized distance between the measured inputs; no complete past wait is
|
|
296
|
+
* returned as the forecast. A known cache/source state must also be known in
|
|
297
|
+
* the training observation, otherwise that observation is not comparable.
|
|
305
298
|
*
|
|
306
299
|
* @param {object} features
|
|
307
300
|
* @returns {number | null}
|
|
308
301
|
*/
|
|
309
302
|
firstFrameSeconds(features) {
|
|
310
303
|
if (!features || typeof features !== "object") return null;
|
|
311
|
-
const
|
|
312
|
-
|
|
304
|
+
const candidates = (this.#entries.firstFrame[this.#firstFrameKey()] ?? [])
|
|
305
|
+
.filter(reading => reading.extension === (features.extension ?? "") && reading.stages);
|
|
313
306
|
if (candidates.length === 0) return null;
|
|
314
307
|
const dimensions = ["fileLength", "downloadSpeed", "linkMbps", "linkSampleAgeMs", "preparedOutputBytes",
|
|
315
308
|
"fileAvailable", "headerRemaining", "peers", "videoOperation", "audioOperation", "videoSpeedX", "audioSpeedX"];
|
|
316
|
-
const
|
|
317
|
-
|
|
309
|
+
const pointsOf = reading => Array.isArray(reading.featureSnapshots) && reading.featureSnapshots.length > 0
|
|
310
|
+
? reading.featureSnapshots : [reading];
|
|
311
|
+
const scales = new Map(dimensions.map(key => [key, median(candidates.flatMap(reading => pointsOf(reading)
|
|
312
|
+
.map(point => FIRST_FRAME_CATEGORICAL.has(key) ? null : nonNegative(point[key])))
|
|
318
313
|
.filter(value => value !== null && value > 0))]));
|
|
319
|
-
|
|
314
|
+
const distances = [];
|
|
320
315
|
for (const reading of candidates) {
|
|
321
|
-
let
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
316
|
+
let bestDistance = null;
|
|
317
|
+
for (const point of pointsOf(reading)) {
|
|
318
|
+
if ((features.preparedOutputBytes !== null && features.preparedOutputBytes !== undefined &&
|
|
319
|
+
point.preparedOutputBytes === null) ||
|
|
320
|
+
[...FIRST_FRAME_CATEGORICAL].some(key => {
|
|
321
|
+
const current = key === "fileAvailable" ? features[key] : operationOf(features[key]);
|
|
322
|
+
return current !== null && current !== undefined && point[key] !== current;
|
|
323
|
+
})) continue;
|
|
324
|
+
let distance = 0;
|
|
325
|
+
let used = 0;
|
|
326
|
+
let shared = 0;
|
|
327
|
+
for (const key of dimensions) {
|
|
328
|
+
const current = FIRST_FRAME_CATEGORICAL.has(key)
|
|
329
|
+
? key === "fileAvailable" ? (typeof features[key] === "boolean" ? features[key] : null)
|
|
330
|
+
: operationOf(features[key])
|
|
331
|
+
: nonNegative(features[key]);
|
|
332
|
+
const previous = FIRST_FRAME_CATEGORICAL.has(key)
|
|
333
|
+
? key === "fileAvailable" ? (typeof point[key] === "boolean" ? point[key] : null)
|
|
334
|
+
: operationOf(point[key])
|
|
335
|
+
: nonNegative(point[key]);
|
|
336
|
+
if (current === null && previous === null) continue;
|
|
337
|
+
if (current === null || previous === null) {
|
|
338
|
+
distance += 1;
|
|
339
|
+
used += 1;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
shared += 1;
|
|
343
|
+
const scale = scales.get(key);
|
|
344
|
+
const difference = typeof current !== "number" && typeof previous !== "number"
|
|
345
|
+
? Number(current !== previous)
|
|
346
|
+
: scale === null
|
|
347
|
+
? Number(current !== previous)
|
|
348
|
+
: Math.log1p(current / scale) - Math.log1p(previous / scale);
|
|
349
|
+
distance += difference * difference;
|
|
336
350
|
used += 1;
|
|
337
|
-
continue;
|
|
338
351
|
}
|
|
339
|
-
shared
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
? Number(current !== previous)
|
|
343
|
-
: scale === null
|
|
344
|
-
? Number(current !== previous)
|
|
345
|
-
: Math.log1p(current / scale) - Math.log1p(previous / scale);
|
|
346
|
-
distance += difference * difference;
|
|
347
|
-
used += 1;
|
|
348
|
-
}
|
|
349
|
-
if (used === 0 || shared === 0) continue;
|
|
350
|
-
distance /= used;
|
|
351
|
-
if (!best || distance < best.distance || (distance === best.distance && reading.at > best.reading.at)) {
|
|
352
|
-
best = { distance, reading };
|
|
352
|
+
if (used === 0 || shared === 0) continue;
|
|
353
|
+
distance /= used;
|
|
354
|
+
if (bestDistance === null || distance < bestDistance) bestDistance = distance;
|
|
353
355
|
}
|
|
356
|
+
if (bestDistance !== null) distances.push({ distance: bestDistance, reading });
|
|
354
357
|
}
|
|
355
|
-
|
|
358
|
+
if (distances.length === 0) return null;
|
|
359
|
+
const exact = distances.filter(candidate => candidate.distance === 0);
|
|
360
|
+
const weighted = exact.length > 0
|
|
361
|
+
? exact.map(candidate => ({ ...candidate, weight: 1 }))
|
|
362
|
+
: distances.map(candidate => ({ ...candidate, weight: 1 / candidate.distance }));
|
|
363
|
+
const stages = ["transportMs", "planMs", "prepareMs", "prebufferMs", "presentationMs"];
|
|
364
|
+
const predicted = stages.map(stage => {
|
|
365
|
+
const applicable = weighted.filter(candidate => Number.isFinite(candidate.reading.stages[stage]));
|
|
366
|
+
const totalWeight = applicable.reduce((sum, candidate) => sum + candidate.weight, 0);
|
|
367
|
+
return totalWeight > 0
|
|
368
|
+
? applicable.reduce((sum, candidate) => sum + candidate.reading.stages[stage] * candidate.weight, 0) / totalWeight
|
|
369
|
+
: null;
|
|
370
|
+
});
|
|
371
|
+
if (predicted.some(value => value === null)) return null;
|
|
372
|
+
return predicted.reduce((sum, value) => sum + value, 0) / 1000;
|
|
356
373
|
}
|
|
357
374
|
|
|
358
375
|
#firstFrameKey() {
|
|
@@ -470,6 +487,33 @@ function operationOf(value) {
|
|
|
470
487
|
return value === "copy" || value === "encode" ? value : null;
|
|
471
488
|
}
|
|
472
489
|
|
|
490
|
+
/** @param {unknown} value @returns {{ totalMs: number, transportMs: number, planMs: number, prepareMs: number, prebufferMs: number, presentationMs: number } | null} */
|
|
491
|
+
function stagesOf(value) {
|
|
492
|
+
if (!value || typeof value !== "object") return null;
|
|
493
|
+
const keys = ["totalMs", "transportMs", "planMs", "prepareMs", "prebufferMs", "presentationMs"];
|
|
494
|
+
const stages = Object.fromEntries(keys.map(key => [key, nonNegative(value[key])]));
|
|
495
|
+
return keys.every(key => stages[key] !== null) ? stages : null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** @param {object} features @returns {object} */
|
|
499
|
+
function firstFrameFeatures(features) {
|
|
500
|
+
return {
|
|
501
|
+
extension: typeof features.extension === "string" ? features.extension : "",
|
|
502
|
+
fileLength: positive(features.fileLength),
|
|
503
|
+
downloadSpeed: nonNegative(features.downloadSpeed),
|
|
504
|
+
linkMbps: nonNegative(features.linkMbps),
|
|
505
|
+
linkSampleAgeMs: nonNegative(features.linkSampleAgeMs),
|
|
506
|
+
preparedOutputBytes: nonNegative(features.preparedOutputBytes),
|
|
507
|
+
fileAvailable: typeof features.fileAvailable === "boolean" ? features.fileAvailable : null,
|
|
508
|
+
headerRemaining: nonNegative(features.headerRemaining),
|
|
509
|
+
peers: nonNegative(features.peers),
|
|
510
|
+
videoOperation: operationOf(features.videoOperation),
|
|
511
|
+
audioOperation: operationOf(features.audioOperation),
|
|
512
|
+
videoSpeedX: nonNegative(features.videoSpeedX),
|
|
513
|
+
audioSpeedX: nonNegative(features.audioSpeedX)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
473
517
|
/** @param {number[]} values @returns {number | null} */
|
|
474
518
|
function median(values) {
|
|
475
519
|
if (values.length === 0) return null;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { logger } from "../../utils/logger.js";
|
|
11
|
-
import { isOutputName } from "../encode/output/index.js";
|
|
11
|
+
import { isOutputName, OutputSpec } from "../encode/output/index.js";
|
|
12
12
|
import { withSubtitleReadiness } from "./subtitle-readiness.js";
|
|
13
13
|
|
|
14
14
|
function isWarmupTimeoutError(error) {
|
|
@@ -190,7 +190,10 @@ export class ViewerRequests {
|
|
|
190
190
|
const changed = this.#recordEarlyWaitSnapshot(context, context.features);
|
|
191
191
|
if (sameWait && changed) context.revision += 1;
|
|
192
192
|
this.#pruneEarlyWaits();
|
|
193
|
-
const
|
|
193
|
+
const measuredWorkSeconds = processing.firstRequiredSegmentsSeconds;
|
|
194
|
+
const totalSeconds = Number.isFinite(measuredWorkSeconds) && measuredWorkSeconds >= 0
|
|
195
|
+
? waitElapsedSince(context, waitElapsedMs) / 1000 + measuredWorkSeconds
|
|
196
|
+
: this.#host.firstFrameSecondsOf?.(context.features) ?? null;
|
|
194
197
|
const seconds = earlyWaitRemainingSeconds(totalSeconds, waitElapsedSince(context, waitElapsedMs));
|
|
195
198
|
if (!Number.isFinite(seconds) || seconds < 0) return null;
|
|
196
199
|
return { version: 1, waitId, revision: context.revision, remainingSeconds: seconds, ageMs: 0 };
|
|
@@ -201,16 +204,20 @@ export class ViewerRequests {
|
|
|
201
204
|
* viewing. The saved source and link readings are the ones available at
|
|
202
205
|
* selection time, before the full track plan existed.
|
|
203
206
|
*
|
|
204
|
-
* @param {{ consumerId: string, generation: number, waitId: string, wait: number, elapsedMs: number
|
|
207
|
+
* @param {{ consumerId: string, generation: number, waitId: string, wait: number, elapsedMs: number,
|
|
208
|
+
* stages?: { totalMs: number, transportMs: number, planMs: number, prepareMs: number,
|
|
209
|
+
* prebufferMs: number, presentationMs: number } | null }} params
|
|
205
210
|
* @returns {void}
|
|
206
211
|
*/
|
|
207
|
-
noteFirstFrameObservation({ consumerId, generation, waitId, wait, elapsedMs }) {
|
|
212
|
+
noteFirstFrameObservation({ consumerId, generation, waitId, wait, elapsedMs, stages = null }) {
|
|
208
213
|
const key = `${consumerId}:${waitId}`;
|
|
209
214
|
const early = this.#earlyWaits.get(key);
|
|
210
215
|
if (!early || early.generation !== generation || !Number.isSafeInteger(wait) || wait < 0 || early.observedWaits.has(wait) ||
|
|
211
216
|
!Number.isFinite(elapsedMs) || elapsedMs < 0) return;
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
const initialFeatures = early.snapshots[0];
|
|
218
|
+
if (initialFeatures) {
|
|
219
|
+
this.#host.noteFirstFrame?.({ seconds: elapsedMs / 1000, features: initialFeatures,
|
|
220
|
+
featureSnapshots: early.snapshots.slice(1), stages });
|
|
214
221
|
}
|
|
215
222
|
early.observedWaits.add(wait);
|
|
216
223
|
early.updatedAt = Date.now();
|
|
@@ -259,11 +266,21 @@ export class ViewerRequests {
|
|
|
259
266
|
if (!outputs?.values || !segmentStore?.sizesOf) return null;
|
|
260
267
|
let bytes = 0;
|
|
261
268
|
const seen = new Set();
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
269
|
+
const liveOutputs = [...outputs.values()];
|
|
270
|
+
const keys = new Set([
|
|
271
|
+
...liveOutputs.map(output => output.outputKey),
|
|
272
|
+
...(segmentStore.addresses?.() ?? [])
|
|
273
|
+
].filter(key => typeof key === "string" && key.length > 0));
|
|
274
|
+
for (const outputKey of keys) {
|
|
275
|
+
const output = liveOutputs.find(candidate => candidate.outputKey === outputKey);
|
|
276
|
+
const spec = output ? null : OutputSpec.fromKey(outputKey);
|
|
277
|
+
const matchesFile = output
|
|
278
|
+
? output.file?.sourceKey === sourceKey && output.file?.fileIndex === fileIndex
|
|
279
|
+
: spec?.sourceKey === sourceKey &&
|
|
280
|
+
(spec.video?.fileIndex === fileIndex || spec.audio?.fileIndex === fileIndex);
|
|
281
|
+
if (!matchesFile || seen.has(outputKey)) continue;
|
|
282
|
+
seen.add(outputKey);
|
|
283
|
+
for (const size of segmentStore.sizesOf(outputKey).values()) {
|
|
267
284
|
if (Number.isFinite(size) && size > 0) bytes += size;
|
|
268
285
|
}
|
|
269
286
|
}
|
|
@@ -976,7 +993,8 @@ export class ViewerRequests {
|
|
|
976
993
|
videoOperation: videoOperation?.operation,
|
|
977
994
|
audioOperation: audioOperation?.operation,
|
|
978
995
|
videoSpeedX: videoOperation?.speed,
|
|
979
|
-
audioSpeedX: audioOperation?.speed
|
|
996
|
+
audioSpeedX: audioOperation?.speed,
|
|
997
|
+
firstRequiredSegmentsSeconds: forecast.firstRequiredSegmentsSeconds
|
|
980
998
|
} });
|
|
981
999
|
if (waitEstimate) forecast.waitEstimate = waitEstimate;
|
|
982
1000
|
}
|
|
@@ -542,6 +542,15 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
542
542
|
...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
|
|
543
543
|
}
|
|
544
544
|
}
|
|
545
|
+
const firstRequiredTrackSeconds = trackState.slice(0, requiredTracks).map((state, trackIndex) => {
|
|
546
|
+
const first = state.segments.find(segment => segment.endSeconds > position);
|
|
547
|
+
if (!first) return 0;
|
|
548
|
+
if (first.ranges.every(range => contains(state.clientRanges, range.start, range.end))) return 0;
|
|
549
|
+
const completion = completions.find(item => item.trackIndex === trackIndex && item.segment.index === first.index);
|
|
550
|
+
return completion?.at ?? null;
|
|
551
|
+
});
|
|
552
|
+
const firstRequiredSegmentsSeconds = firstRequiredTrackSeconds.every(Number.isFinite)
|
|
553
|
+
? Math.max(0, ...firstRequiredTrackSeconds) : null;
|
|
545
554
|
const delay = Math.max(0, ...constraints);
|
|
546
555
|
const originReady = trackState.every(({ clientRanges }) => compare(contiguousEnd(clientRanges, at), at) > 0);
|
|
547
556
|
const ready = delay === 0 && originReady;
|
|
@@ -549,6 +558,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
549
558
|
Math.max(0, reserve - buffered), ready ? 'trajectory-safe-now' :
|
|
550
559
|
Number.isFinite(delay) ? 'minimum-safe-delay' : 'service-not-advancing', preparedSegments);
|
|
551
560
|
return { ...forecast, measuredAt: now, predictedStartAt: Number.isFinite(delay) ? now + delay * 1000 : null,
|
|
561
|
+
firstRequiredSegmentsSeconds,
|
|
552
562
|
// The piece whose `kind` — its production, or its delivery to the page —
|
|
553
563
|
// comes `atSeconds` from now while the media before it lasts
|
|
554
564
|
// `aheadSeconds`: the difference is the delay.
|