@torrent-tv/proxy 2.101.29 → 2.101.31
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,30 @@ export class LocalObservations {
|
|
|
297
290
|
}
|
|
298
291
|
|
|
299
292
|
/**
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* data contributes distance only when the other observation measured it.
|
|
293
|
+
* Price the early wait from separate measured stage medians for the same
|
|
294
|
+
* output path. Numeric proximity of a past file, source rate, or link rate
|
|
295
|
+
* does not select a completed run. Each observation contributes once, using
|
|
296
|
+
* its latest snapshot that states the current cache and operation path.
|
|
305
297
|
*
|
|
306
298
|
* @param {object} features
|
|
307
299
|
* @returns {number | null}
|
|
308
300
|
*/
|
|
309
301
|
firstFrameSeconds(features) {
|
|
310
302
|
if (!features || typeof features !== "object") return null;
|
|
311
|
-
const
|
|
312
|
-
const candidates =
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
.
|
|
319
|
-
let best = null;
|
|
320
|
-
for (const reading of candidates) {
|
|
321
|
-
let distance = 0;
|
|
322
|
-
let used = 0;
|
|
323
|
-
let shared = 0;
|
|
324
|
-
for (const key of dimensions) {
|
|
325
|
-
const current = FIRST_FRAME_CATEGORICAL.has(key)
|
|
326
|
-
? key === "fileAvailable" ? (typeof features[key] === "boolean" ? features[key] : null)
|
|
327
|
-
: operationOf(features[key])
|
|
328
|
-
: nonNegative(features[key]);
|
|
329
|
-
const previous = FIRST_FRAME_CATEGORICAL.has(key)
|
|
330
|
-
? key === "fileAvailable" ? (typeof reading[key] === "boolean" ? reading[key] : null)
|
|
331
|
-
: operationOf(reading[key])
|
|
332
|
-
: nonNegative(reading[key]);
|
|
333
|
-
if (current === null && previous === null) continue;
|
|
334
|
-
if (current === null || previous === null) {
|
|
335
|
-
distance += 1;
|
|
336
|
-
used += 1;
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
shared += 1;
|
|
340
|
-
const scale = scales.get(key);
|
|
341
|
-
const difference = typeof current !== "number" && typeof previous !== "number"
|
|
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 };
|
|
353
|
-
}
|
|
303
|
+
const stages = ["transportMs", "planMs", "prepareMs", "prebufferMs", "presentationMs"];
|
|
304
|
+
const candidates = [];
|
|
305
|
+
for (const reading of this.#entries.firstFrame[this.#firstFrameKey()] ?? []) {
|
|
306
|
+
if (reading.extension !== (features.extension ?? "") || !reading.stages) continue;
|
|
307
|
+
const points = Array.isArray(reading.featureSnapshots) && reading.featureSnapshots.length > 0
|
|
308
|
+
? reading.featureSnapshots : [reading];
|
|
309
|
+
const point = [...points].reverse().find(candidate => firstFramePathMatches(features, candidate));
|
|
310
|
+
if (point) candidates.push(reading.stages);
|
|
354
311
|
}
|
|
355
|
-
|
|
312
|
+
if (candidates.length === 0) return null;
|
|
313
|
+
const prices = stages.map(stage => median(candidates.map(reading => reading[stage])
|
|
314
|
+
.filter(Number.isFinite)));
|
|
315
|
+
if (prices.some(value => value === null)) return null;
|
|
316
|
+
return prices.reduce((sum, value) => sum + value, 0) / 1000;
|
|
356
317
|
}
|
|
357
318
|
|
|
358
319
|
#firstFrameKey() {
|
|
@@ -470,6 +431,47 @@ function operationOf(value) {
|
|
|
470
431
|
return value === "copy" || value === "encode" ? value : null;
|
|
471
432
|
}
|
|
472
433
|
|
|
434
|
+
/** @param {object} current @param {object} previous @returns {boolean} */
|
|
435
|
+
function firstFramePathMatches(current, previous) {
|
|
436
|
+
for (const key of FIRST_FRAME_CATEGORICAL) {
|
|
437
|
+
const expected = key === "fileAvailable" ? current[key] : operationOf(current[key]);
|
|
438
|
+
if (expected !== null && expected !== undefined && previous[key] !== expected) return false;
|
|
439
|
+
}
|
|
440
|
+
const preparedBytes = nonNegative(current.preparedOutputBytes);
|
|
441
|
+
if (preparedBytes !== null) {
|
|
442
|
+
const previousBytes = nonNegative(previous.preparedOutputBytes);
|
|
443
|
+
if (previousBytes === null || (previousBytes > 0) !== (preparedBytes > 0)) return false;
|
|
444
|
+
}
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** @param {unknown} value @returns {{ totalMs: number, transportMs: number, planMs: number, prepareMs: number, prebufferMs: number, presentationMs: number } | null} */
|
|
449
|
+
function stagesOf(value) {
|
|
450
|
+
if (!value || typeof value !== "object") return null;
|
|
451
|
+
const keys = ["totalMs", "transportMs", "planMs", "prepareMs", "prebufferMs", "presentationMs"];
|
|
452
|
+
const stages = Object.fromEntries(keys.map(key => [key, nonNegative(value[key])]));
|
|
453
|
+
return keys.every(key => stages[key] !== null) ? stages : null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** @param {object} features @returns {object} */
|
|
457
|
+
function firstFrameFeatures(features) {
|
|
458
|
+
return {
|
|
459
|
+
extension: typeof features.extension === "string" ? features.extension : "",
|
|
460
|
+
fileLength: positive(features.fileLength),
|
|
461
|
+
downloadSpeed: nonNegative(features.downloadSpeed),
|
|
462
|
+
linkMbps: nonNegative(features.linkMbps),
|
|
463
|
+
linkSampleAgeMs: nonNegative(features.linkSampleAgeMs),
|
|
464
|
+
preparedOutputBytes: nonNegative(features.preparedOutputBytes),
|
|
465
|
+
fileAvailable: typeof features.fileAvailable === "boolean" ? features.fileAvailable : null,
|
|
466
|
+
headerRemaining: nonNegative(features.headerRemaining),
|
|
467
|
+
peers: nonNegative(features.peers),
|
|
468
|
+
videoOperation: operationOf(features.videoOperation),
|
|
469
|
+
audioOperation: operationOf(features.audioOperation),
|
|
470
|
+
videoSpeedX: nonNegative(features.videoSpeedX),
|
|
471
|
+
audioSpeedX: nonNegative(features.audioSpeedX)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
473
475
|
/** @param {number[]} values @returns {number | null} */
|
|
474
476
|
function median(values) {
|
|
475
477
|
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.
|