@torrent-tv/proxy 2.107.49 → 2.107.51
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 +1 -1
- package/services/{server/WaitLedger.js → encode/OutputWaitLedger.js} +37 -7
- package/services/encode/output/OutputCatalog.js +46 -2
- package/services/server/OutputLifecycle.js +2 -1
- package/services/server/SegmentServing.js +14 -0
- package/services/server/ViewerRequests.js +14 -54
- package/services/server/transcode-session-files.js +9 -4
- package/services/server/wire-outputs.js +16 -3
- package/services/viewer/Viewer.js +16 -0
- package/services/viewer/choices.js +16 -0
package/package.json
CHANGED
|
@@ -58,6 +58,23 @@ export class WaitLedger {
|
|
|
58
58
|
/** @type {Map<string, Map<string, number>>} */
|
|
59
59
|
#counts = new Map();
|
|
60
60
|
|
|
61
|
+
/** @type {Map<string, object>} */
|
|
62
|
+
#owners = new Map();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Begin a live output-key lifetime. Reopening a key starts a clean history;
|
|
66
|
+
* the returned identity lets a held response prove that it still belongs to
|
|
67
|
+
* this lifetime when it eventually completes.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} key
|
|
70
|
+
* @param {object} identity
|
|
71
|
+
*/
|
|
72
|
+
open(key, identity) {
|
|
73
|
+
if (!key || !identity || this.#owners.get(key) === identity) return;
|
|
74
|
+
this.forget(key);
|
|
75
|
+
this.#owners.set(key, identity);
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
/**
|
|
62
79
|
* Somebody waited this long for something the map ranked this highly.
|
|
63
80
|
*
|
|
@@ -66,11 +83,13 @@ export class WaitLedger {
|
|
|
66
83
|
* @param {number} rank - The map's rank for the thing waited for.
|
|
67
84
|
* @param {number} topRank - The highest rank the map states, so the rank can
|
|
68
85
|
* be read as a position rather than as an absolute number.
|
|
86
|
+
* @param {object} [identity] - The output-key lifetime that started this wait.
|
|
69
87
|
*/
|
|
70
|
-
note(key, waitedMs, rank, topRank) {
|
|
88
|
+
note(key, waitedMs, rank, topRank, identity = undefined) {
|
|
71
89
|
if (!key || !Number.isFinite(waitedMs) || waitedMs < 0) {
|
|
72
90
|
return;
|
|
73
91
|
}
|
|
92
|
+
if (identity !== undefined && this.#owners.get(key) !== identity) return;
|
|
74
93
|
const band = bandOf(rank, topRank);
|
|
75
94
|
let byBand = this.#waits.get(key);
|
|
76
95
|
if (!byBand) {
|
|
@@ -128,15 +147,26 @@ export class WaitLedger {
|
|
|
128
147
|
|
|
129
148
|
/**
|
|
130
149
|
* @param {string} key
|
|
150
|
+
* @param {object} [identity] - Ignore release from an older output lifetime.
|
|
131
151
|
*/
|
|
132
|
-
forget(key) {
|
|
152
|
+
forget(key, identity = undefined) {
|
|
153
|
+
if (identity !== undefined && this.#owners.get(key) !== identity) return false;
|
|
133
154
|
this.#waits.delete(key);
|
|
134
155
|
this.#counts.delete(key);
|
|
156
|
+
if (identity === undefined || this.#owners.get(key) === identity) this.#owners.delete(key);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Release every record owned by this proxy before its shutdown completes. */
|
|
161
|
+
clear() {
|
|
162
|
+
this.#waits.clear();
|
|
163
|
+
this.#counts.clear();
|
|
164
|
+
this.#owners.clear();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Number of output keys with retained measurements. */
|
|
168
|
+
get size() {
|
|
169
|
+
return this.#waits.size;
|
|
135
170
|
}
|
|
136
171
|
}
|
|
137
172
|
|
|
138
|
-
/**
|
|
139
|
-
* One ledger for the process, because the question is asked in two layers and
|
|
140
|
-
* the answer is comparable only if the scale is the same.
|
|
141
|
-
*/
|
|
142
|
-
export const waits = new WaitLedger();
|
|
@@ -26,7 +26,11 @@ export class OutputCatalog {
|
|
|
26
26
|
|
|
27
27
|
#byId = new Map();
|
|
28
28
|
#lifetime = new Map();
|
|
29
|
+
/** @type {Map<string, object>} */
|
|
30
|
+
#waitIdentities = new Map();
|
|
29
31
|
#now;
|
|
32
|
+
#waitIdentityStarted;
|
|
33
|
+
#waitIdentityEnded;
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
36
|
* @param {object} [params]
|
|
@@ -35,11 +39,15 @@ export class OutputCatalog {
|
|
|
35
39
|
* source file is, which the torrent reports.
|
|
36
40
|
* @param {(address: string) => { index: number, size: number }} [params.largestPieceOf]
|
|
37
41
|
* The biggest piece an output has made, which the disk knows.
|
|
42
|
+
* @param {(address: string, identity: object) => void} [params.waitIdentityStarted]
|
|
43
|
+
* @param {(address: string, identity: object) => void} [params.waitIdentityEnded]
|
|
38
44
|
*/
|
|
39
45
|
constructor({
|
|
40
46
|
now = Date.now,
|
|
41
47
|
fileLengthOf = () => 0,
|
|
42
|
-
largestPieceOf = () => ({ index: -1, size: 0 })
|
|
48
|
+
largestPieceOf = () => ({ index: -1, size: 0 }),
|
|
49
|
+
waitIdentityStarted = () => {},
|
|
50
|
+
waitIdentityEnded = () => {}
|
|
43
51
|
} = {}) {
|
|
44
52
|
this.#now = now;
|
|
45
53
|
// Two facts this catalog needs and does not own, taken as plain functions
|
|
@@ -48,15 +56,22 @@ export class OutputCatalog {
|
|
|
48
56
|
// it is part of the output itself (`OutputSpec`).
|
|
49
57
|
this.fileLengthOf = fileLengthOf;
|
|
50
58
|
this.largestPieceOf = largestPieceOf;
|
|
59
|
+
this.#waitIdentityStarted = waitIdentityStarted;
|
|
60
|
+
this.#waitIdentityEnded = waitIdentityEnded;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
set(id, output) {
|
|
54
64
|
const known = this.#byId.get(id);
|
|
65
|
+
const oldAddress = known?.outputKey;
|
|
55
66
|
this.#byId.set(id, output);
|
|
56
67
|
if (known !== output) {
|
|
57
68
|
const at = this.#now();
|
|
58
69
|
this.#lifetime.set(id, { startedAt: at, lastAccessedAt: at });
|
|
59
70
|
}
|
|
71
|
+
if (known !== output && oldAddress !== output?.outputKey) {
|
|
72
|
+
this.#forgetWaitIdentityIfUnused(oldAddress);
|
|
73
|
+
}
|
|
74
|
+
this.#openWaitIdentity(output?.outputKey);
|
|
60
75
|
return this;
|
|
61
76
|
}
|
|
62
77
|
|
|
@@ -69,13 +84,42 @@ export class OutputCatalog {
|
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
delete(id) {
|
|
87
|
+
const output = this.#byId.get(id);
|
|
72
88
|
this.#lifetime.delete(id);
|
|
73
|
-
|
|
89
|
+
const deleted = this.#byId.delete(id);
|
|
90
|
+
if (deleted) this.#forgetWaitIdentityIfUnused(output?.outputKey);
|
|
91
|
+
return deleted;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
clear() {
|
|
95
|
+
const addresses = new Set([...this.#byId.values()].map(output => output?.outputKey).filter(Boolean));
|
|
77
96
|
this.#lifetime.clear();
|
|
78
97
|
this.#byId.clear();
|
|
98
|
+
for (const address of addresses) this.#forgetWaitIdentityIfUnused(address);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The shared active lifetime for the output addressed by a live session. */
|
|
102
|
+
waitIdentity(id) {
|
|
103
|
+
const output = this.#byId.get(id);
|
|
104
|
+
return output ? this.#waitIdentities.get(output.outputKey) ?? null : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#openWaitIdentity(address) {
|
|
108
|
+
if (typeof address !== "string" || !address || this.#waitIdentities.has(address)) return;
|
|
109
|
+
const identity = {};
|
|
110
|
+
this.#waitIdentities.set(address, identity);
|
|
111
|
+
this.#waitIdentityStarted(address, identity);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#forgetWaitIdentityIfUnused(address) {
|
|
115
|
+
if (typeof address !== "string" || !address) return;
|
|
116
|
+
for (const output of this.#byId.values()) {
|
|
117
|
+
if (output?.outputKey === address) return;
|
|
118
|
+
}
|
|
119
|
+
const identity = this.#waitIdentities.get(address);
|
|
120
|
+
if (!identity) return;
|
|
121
|
+
this.#waitIdentities.delete(address);
|
|
122
|
+
this.#waitIdentityEnded(address, identity);
|
|
79
123
|
}
|
|
80
124
|
|
|
81
125
|
touch(outputOrId, at = this.#now()) {
|
|
@@ -55,7 +55,7 @@ export class OutputLifecycle {
|
|
|
55
55
|
#host;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
* @param {object} host - `retention`, `outputNeeded`, `outputWriting`, `outputReading`, `planEncodersSoon`, `now`, `invalidateWaits`, `segmentFormatOfKey`, `viewerSegmentsOn`, `budgetTimer`, `cleanupTimer`, `encodeRuns`, `keyframeTables`, `machineBudget`, `outputTimes`, `outputs`, `returns`, `segmentStore`, `sessionTtlMs`, `sourceFiles`, `timelines`, `viewers`
|
|
58
|
+
* @param {object} host - `retention`, `outputNeeded`, `outputWriting`, `outputReading`, `planEncodersSoon`, `now`, `invalidateWaits`, `clearWaits`, `segmentFormatOfKey`, `viewerSegmentsOn`, `budgetTimer`, `cleanupTimer`, `encodeRuns`, `keyframeTables`, `machineBudget`, `outputTimes`, `outputs`, `returns`, `segmentStore`, `sessionTtlMs`, `sourceFiles`, `timelines`, `viewers`
|
|
59
59
|
*/
|
|
60
60
|
constructor(host) {
|
|
61
61
|
this.#host = host;
|
|
@@ -267,6 +267,7 @@ export class OutputLifecycle {
|
|
|
267
267
|
for (const sessionId of activeIds) {
|
|
268
268
|
await this.disposeSession(sessionId);
|
|
269
269
|
}
|
|
270
|
+
this.#host.clearWaits?.();
|
|
270
271
|
// Everything this process owns, root included. See SegmentStore.dropAll.
|
|
271
272
|
this.#host.segmentStore.dropAll("the proxy is shutting down");
|
|
272
273
|
}
|
|
@@ -78,6 +78,20 @@ export class SegmentServing {
|
|
|
78
78
|
state.waitListeners.clear();
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Identity of the live output-key lifetime named by this session. */
|
|
82
|
+
waitIdentity(sessionId) {
|
|
83
|
+
return this.#host.outputs.waitIdentity(sessionId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Record a completed wait through the ledger operation wired by the server. */
|
|
87
|
+
noteWait(ranked, waitedMs, identity) {
|
|
88
|
+
this.#host.noteWait?.(ranked, waitedMs, identity);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
waitBandOf(rank, topRank) {
|
|
92
|
+
return this.#host.waitBandOf?.(rank, topRank) ?? "later";
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
/** Observe storage and lifecycle before a request checks file availability. */
|
|
82
96
|
subscribeFileChange(sessionId) {
|
|
83
97
|
const output = isOutputName(sessionId) ? this.#host.outputs.get(sessionId) : null;
|
|
@@ -154,7 +154,7 @@ export class ViewerRequests {
|
|
|
154
154
|
#forecastPieces = new WeakMap();
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* @param {object} host - `opening`, `activeOutputFor`, `viewerSecondsOn`, `minimumBufferSecondsFor`, `getSourceStats`, `planEncodersSoon`, `encodeRuns`, `encodeSpeedReadingsOf`, `
|
|
157
|
+
* @param {object} host - `opening`, `activeOutputFor`, `viewerSecondsOn`, `minimumBufferSecondsFor`, `getSourceStats`, `planEncodersSoon`, `encodeRuns`, `encodeSpeedReadingsOf`, `outputs`, `lookaheadSeconds`, `quality`, `qualityOffer`, `renditions`, `segmentDurationSec`, `segmentStore`, `sourceFiles`, `startupWaitMs`, `viewers`, `placeViewerOn`, `chooseAudioTrack`, `acceptsGeneration`, `generationOfRequest`, `noteGivenOutput`, `holdForResponse`, `noteServingVerdict`, `servingVerdictOf`
|
|
158
158
|
*/
|
|
159
159
|
constructor(host) {
|
|
160
160
|
this.#host = host;
|
|
@@ -351,6 +351,7 @@ export class ViewerRequests {
|
|
|
351
351
|
// Whether they were already on the output this request lands on, read at
|
|
352
352
|
// the moment they are put on it.
|
|
353
353
|
let alreadyOn = false;
|
|
354
|
+
let placedHere = false;
|
|
354
355
|
// THE PLACE ON THIS MACHINE, AND THE VIEWER IN IT, in one synchronous call
|
|
355
356
|
// the opening makes the moment it has the output (roadmap item 97, step 14).
|
|
356
357
|
// Asked and placed together, so the next viewer to open anything is asked
|
|
@@ -367,7 +368,7 @@ export class ViewerRequests {
|
|
|
367
368
|
: this.#host.admitsWatching(candidate);
|
|
368
369
|
if (answer.admitted) {
|
|
369
370
|
known = this.#host.viewers.of(candidate, consumerId);
|
|
370
|
-
this.
|
|
371
|
+
placedHere = this.placeViewerOn(candidate, consumerId, startPositionSeconds) === true;
|
|
371
372
|
}
|
|
372
373
|
return answer;
|
|
373
374
|
}
|
|
@@ -385,7 +386,7 @@ export class ViewerRequests {
|
|
|
385
386
|
// browser may need a track re-encoded that the first viewer's could
|
|
386
387
|
// decode as it stands.
|
|
387
388
|
const viewer = this.#host.viewers.of(output, consumerId);
|
|
388
|
-
|
|
389
|
+
this.#host.chooseAudioTrack(output, consumerId, audio);
|
|
389
390
|
// The picture as their page sees it, which bounds the height made for
|
|
390
391
|
// them from here on (roadmap item 98).
|
|
391
392
|
viewer.noteVisiblePicture(request.visiblePicture);
|
|
@@ -400,7 +401,7 @@ export class ViewerRequests {
|
|
|
400
401
|
: null,
|
|
401
402
|
bufferedAheadSec: request.bufferedAheadSec,
|
|
402
403
|
bufferLimitSeconds: request.bufferLimitSeconds,
|
|
403
|
-
positionSeconds: startPositionSeconds,
|
|
404
|
+
...(placedHere ? { positionSeconds: startPositionSeconds } : {}),
|
|
404
405
|
waiting: true,
|
|
405
406
|
playing: false,
|
|
406
407
|
visiblePicture: request.visiblePicture
|
|
@@ -438,65 +439,24 @@ export class ViewerRequests {
|
|
|
438
439
|
}
|
|
439
440
|
|
|
440
441
|
/**
|
|
441
|
-
*
|
|
442
|
+
* Ask the viewer layer to place this viewer at their initial position.
|
|
442
443
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* request, so a viewer counted as placeless until they had asked for a
|
|
447
|
-
* segment, and an output whose viewers were all placeless had every encoder
|
|
448
|
-
* on it stopped for having nobody. The soundtrack that failed that day could
|
|
449
|
-
* not have asked: the segment it would have asked for needed an `init.mp4`
|
|
450
|
-
* that the stopped encoder was going to make.
|
|
451
|
-
*
|
|
452
|
-
* Only ever places a viewer who has none. A viewer already placed is being
|
|
453
|
-
* kept current by their own requests and seeks, and a fresh create request
|
|
454
|
-
* carries a default of zero that must not drag them back to the beginning.
|
|
455
|
-
*
|
|
456
|
-
* @param {HlsSession} session
|
|
457
|
-
* @param {import("../viewer/Viewer.js").Viewer} viewer
|
|
458
|
-
* @param {number} positionSeconds
|
|
459
|
-
* @returns {void}
|
|
460
|
-
*/
|
|
461
|
-
/**
|
|
462
|
-
* Put a viewer named by id onto this output, where they have no position yet.
|
|
463
|
-
*
|
|
464
|
-
* The same operation as {@link placeViewer}, addressed the way another
|
|
465
|
-
* component may address it: by name. Encoding asks for this when a request
|
|
466
|
-
* for any file of an output is the first sign of the person behind it, and it
|
|
467
|
-
* must not be handed the viewer to do it — a viewer's record has one writer,
|
|
468
|
-
* and that is the viewer layer.
|
|
444
|
+
* Encoding asks for this when a request for any file of an output is the
|
|
445
|
+
* first sign of the person behind it. The operation takes the viewer's name
|
|
446
|
+
* and position; the viewer layer owns the record and its idempotency rule.
|
|
469
447
|
*
|
|
470
448
|
* @param {object} output
|
|
471
449
|
* @param {string} consumerId
|
|
472
450
|
* @param {number} positionSeconds
|
|
473
|
-
* @returns {
|
|
451
|
+
* @returns {boolean} Whether the viewer layer placed the viewer.
|
|
474
452
|
*/
|
|
475
453
|
placeViewerOn(output, consumerId, positionSeconds) {
|
|
476
454
|
if (!output || !consumerId) {
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
const viewer = this.#host.viewers.join(output, consumerId);
|
|
480
|
-
if (viewer) this.placeViewer(output, viewer, positionSeconds);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
placeViewer(session, viewer, positionSeconds) {
|
|
484
|
-
if (viewer.position !== null) {
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
const seconds = Number.isFinite(positionSeconds) && positionSeconds > 0 ? positionSeconds : 0;
|
|
488
|
-
let segment = 0;
|
|
489
|
-
try {
|
|
490
|
-
const index = this.#host.outputTimes.segmentIndexForTime(session, seconds);
|
|
491
|
-
if (Number.isInteger(index) && index >= 0) {
|
|
492
|
-
segment = index;
|
|
493
|
-
}
|
|
494
|
-
} catch {
|
|
495
|
-
// A session whose cut table is not built yet places its viewer at the
|
|
496
|
-
// beginning, which is where the run starts anyway.
|
|
455
|
+
return false;
|
|
497
456
|
}
|
|
498
|
-
|
|
499
|
-
this.#host.planEncodersSoon();
|
|
457
|
+
const placed = this.#host.placeViewerOn(output, consumerId, positionSeconds) === true;
|
|
458
|
+
if (placed) this.#host.planEncodersSoon();
|
|
459
|
+
return placed;
|
|
500
460
|
}
|
|
501
461
|
|
|
502
462
|
/**
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { logger } from "../../utils/logger.js";
|
|
2
|
-
import { bandOf, waits } from "./WaitLedger.js";
|
|
3
|
-
|
|
4
2
|
/**
|
|
5
3
|
* Which viewer is asking, or an empty string where the transport names nobody.
|
|
6
4
|
*
|
|
@@ -84,6 +82,12 @@ export async function serveSessionFile(req, reply, { serving, viewerRequests, se
|
|
|
84
82
|
// a plain HTTP transport, where no loader of ours builds the URL, and then
|
|
85
83
|
// the single shared position decides as it always did.
|
|
86
84
|
const consumerId = consumerOf(req);
|
|
85
|
+
// Hold the output-key lifetime that this request entered. Disposal followed
|
|
86
|
+
// by reopening the same key must not let its old completion write into the
|
|
87
|
+
// new output's measurements.
|
|
88
|
+
const waitIdentity = typeof serving.waitIdentity === "function"
|
|
89
|
+
? serving.waitIdentity(sessionId)
|
|
90
|
+
: undefined;
|
|
87
91
|
// Readiness or a terminal state ends the wait. A caller leaving cancels it;
|
|
88
92
|
// elapsed time is not evidence that media preparation failed.
|
|
89
93
|
const holdStartedAt = Date.now();
|
|
@@ -132,11 +136,12 @@ export async function serveSessionFile(req, reply, { serving, viewerRequests, se
|
|
|
132
136
|
// like, and without them the table would hold only the failures.
|
|
133
137
|
const ranked = result.ranked ?? null;
|
|
134
138
|
if (ranked) {
|
|
135
|
-
|
|
139
|
+
serving.noteWait?.(ranked, heldMs, waitIdentity);
|
|
136
140
|
}
|
|
137
141
|
logger.info(
|
|
138
142
|
`[hold] ${fileName} ${outcome} after ${heldMs}ms` +
|
|
139
|
-
(ranked ? ` (the map wants it ${
|
|
143
|
+
(ranked ? ` (the map wants it ${serving.waitBandOf?.(ranked.rank, ranked.topRank) ?? "later"}, ` +
|
|
144
|
+
`rank ${ranked.rank} of ${ranked.topRank})` : "")
|
|
140
145
|
);
|
|
141
146
|
}
|
|
142
147
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { logger } from "../../utils/logger.js";
|
|
12
12
|
import { KeyframeTables } from "../media/KeyframeTables.js";
|
|
13
|
-
import {
|
|
13
|
+
import { bandOf, WaitLedger } from "../encode/OutputWaitLedger.js";
|
|
14
14
|
import { readEndedChildrenCpuSeconds, readMachineState, readProcessCpuSeconds, readProxyCpuSeconds, readSystemCpu, shareOfMachine } from "../encode/host-load.js";
|
|
15
15
|
import { minimumBufferFrom } from "../torrent/supply-margin.js";
|
|
16
16
|
import { PriorityOrchestrator } from "../viewer/PriorityOrchestrator.js";
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
noteStepBeingWarmed,
|
|
52
52
|
noteStepOnScreen,
|
|
53
53
|
outputsBeingPrepared,
|
|
54
|
+
placeInitially,
|
|
54
55
|
placeOn,
|
|
55
56
|
presentOn,
|
|
56
57
|
qualityModeOf,
|
|
@@ -244,6 +245,9 @@ export function wireOutputs({
|
|
|
244
245
|
// Told when an encoder starts or ends: the room this host has for one more
|
|
245
246
|
// encode changes with it, and the server chooses proxies by that room.
|
|
246
247
|
onEncodersChanged = null}) {
|
|
248
|
+
// Long-lived wait diagnostics belong to this wired output graph: each live
|
|
249
|
+
// output key owns its record, and the graph releases it at the last session.
|
|
250
|
+
const waits = new WaitLedger();
|
|
247
251
|
const parts = {};
|
|
248
252
|
const audioDescriptionOf = (output) => (getCachedAudioTracks?.({
|
|
249
253
|
sourceKey: output.file.sourceKey, fileIndex: output.spec.grid?.fileIndex ?? output.file.fileIndex
|
|
@@ -552,6 +556,9 @@ export function wireOutputs({
|
|
|
552
556
|
get lookaheadSeconds() { return parts.lookaheadSeconds; },
|
|
553
557
|
get outputTimes() { return parts.outputTimes; },
|
|
554
558
|
get outputs() { return parts.outputs; },
|
|
559
|
+
noteWait: (ranked, waitedMs, identity) =>
|
|
560
|
+
waits.note(ranked.address, waitedMs, ranked.rank, ranked.topRank, identity),
|
|
561
|
+
waitBandOf: bandOf,
|
|
555
562
|
get segmentStore() { return parts.segmentStore; },
|
|
556
563
|
get startupWaitMs() { return parts.startupWaitMs; },
|
|
557
564
|
get viewers() { return parts.viewers; },
|
|
@@ -565,6 +572,7 @@ export function wireOutputs({
|
|
|
565
572
|
outputReading: key => parts.segmentStore.isReading(key) || parts.viewers.responsesHold(key),
|
|
566
573
|
planEncodersSoon: () => parts.encodeRuns.planEncodersSoon(),
|
|
567
574
|
invalidateWaits: (output) => parts.serving.invalidateWaits(output),
|
|
575
|
+
clearWaits: () => waits.clear(),
|
|
568
576
|
segmentFormatOfKey,
|
|
569
577
|
// Where the viewers of one output stand, in its segments: what the store
|
|
570
578
|
// the output retention policy reads to give disk back in the right order.
|
|
@@ -675,8 +683,11 @@ export function wireOutputs({
|
|
|
675
683
|
return video + audio;
|
|
676
684
|
},
|
|
677
685
|
get opening() { return parts.opening; },
|
|
678
|
-
get outputTimes() { return parts.outputTimes; },
|
|
679
686
|
get outputs() { return parts.outputs; },
|
|
687
|
+
placeViewerOn: (output, consumerId, seconds, now) =>
|
|
688
|
+
placeInitially(parts.viewers, output, consumerId, seconds, now),
|
|
689
|
+
chooseAudioTrack: (output, consumerId, choice) =>
|
|
690
|
+
chooseAudioTrack(parts.viewers, output, consumerId, choice),
|
|
680
691
|
get lookaheadSeconds() { return parts.lookaheadSeconds; },
|
|
681
692
|
get quality() { return parts.quality; },
|
|
682
693
|
get qualityOffer() { return parts.qualityOffer; },
|
|
@@ -960,7 +971,9 @@ export function wireOutputs({
|
|
|
960
971
|
// belongs to, the steps, the soundtracks, the height a step is named by.
|
|
961
972
|
parts.outputs = new OutputCatalog({
|
|
962
973
|
fileLengthOf: (session) => parts.hostLoad.fileLengthByKey.get(session.file.key) ?? 0,
|
|
963
|
-
largestPieceOf: (address) => parts.segmentStore.largestPiece(address)
|
|
974
|
+
largestPieceOf: (address) => parts.segmentStore.largestPiece(address),
|
|
975
|
+
waitIdentityStarted: (address, identity) => waits.open(address, identity),
|
|
976
|
+
waitIdentityEnded: (address, identity) => waits.forget(address, identity)
|
|
964
977
|
});
|
|
965
978
|
// Where produced segments live, addressed by WHAT they are rather than by
|
|
966
979
|
// which session's encoder wrote them. Two sessions of one output — two
|
|
@@ -427,6 +427,22 @@ export class Viewer {
|
|
|
427
427
|
this.bufferedSeconds = 0;
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Place this viewer at the position supplied by their first output opening.
|
|
432
|
+
* An opening that reuses an output must not reset a position already stated
|
|
433
|
+
* by this viewer. Initial placement has none of the buffer or pause effects
|
|
434
|
+
* of an explicit seek.
|
|
435
|
+
*
|
|
436
|
+
* @param {number} seconds
|
|
437
|
+
* @param {number} [now]
|
|
438
|
+
* @returns {boolean} Whether this call placed the viewer.
|
|
439
|
+
*/
|
|
440
|
+
placeInitially(seconds, now = Date.now()) {
|
|
441
|
+
if (this.position !== null) return false;
|
|
442
|
+
this.#place(Number.isFinite(seconds) && seconds > 0 ? seconds : 0, now);
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
|
|
430
446
|
/**
|
|
431
447
|
* Where they are, stated — without anything a move to another place means.
|
|
432
448
|
*
|
|
@@ -96,10 +96,26 @@ export function audioChoiceOf(viewers, output, consumerId) {
|
|
|
96
96
|
* @returns {void}
|
|
97
97
|
*/
|
|
98
98
|
export function chooseAudioTrack(viewers, output, consumerId, choice) {
|
|
99
|
+
if (!choice) return;
|
|
99
100
|
const viewer = viewers.join(output, consumerId);
|
|
100
101
|
if (viewer) viewer.audio = { trackIndex: choice.trackIndex, transcode: choice.transcode === true };
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Place a viewer at the initial position named by an output opening.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} viewers
|
|
108
|
+
* @param {object} output
|
|
109
|
+
* @param {string} consumerId
|
|
110
|
+
* @param {number} seconds
|
|
111
|
+
* @param {number} [now]
|
|
112
|
+
* @returns {boolean} Whether this call placed the viewer.
|
|
113
|
+
*/
|
|
114
|
+
export function placeInitially(viewers, output, consumerId, seconds, now = Date.now()) {
|
|
115
|
+
const viewer = viewers.join(output, consumerId, now);
|
|
116
|
+
return viewer?.placeInitially(seconds, now) ?? false;
|
|
117
|
+
}
|
|
118
|
+
|
|
103
119
|
/**
|
|
104
120
|
* The step on this viewer's screen, as a session id, or null for the picture
|
|
105
121
|
* itself.
|