@torrent-tv/proxy 2.106.0 → 2.107.0
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
package/server.js
CHANGED
|
@@ -23,6 +23,7 @@ import { trackChoicesOf } from "./services/viewer/segment-demands.js";
|
|
|
23
23
|
import { PlaybackProjection } from "./services/torrent/PlaybackProjection.js";
|
|
24
24
|
import { SegmentInputTable } from "./services/viewer/segment-input-table.js";
|
|
25
25
|
import { SourcePreparation, preparationInterval } from "./services/viewer/SourcePreparation.js";
|
|
26
|
+
import { publishPlaybackBeforePreparationRefresh } from "./services/server/publish-playback-before-preparation-refresh.js";
|
|
26
27
|
import { handleHealthzGet } from "./routes/healthz/get.js";
|
|
27
28
|
import { handleApiDeliverySinkGet } from "./routes/api/delivery-sink/get.js";
|
|
28
29
|
import { handleApiLinkProbeGet } from "./routes/api/link-probe/get.js";
|
|
@@ -1213,7 +1214,6 @@ export async function startProxyServer({
|
|
|
1213
1214
|
setDownloadRate: (bytesPerSecond, because) => torrentPool.setDownloadRate?.(bytesPerSecond, because),
|
|
1214
1215
|
setSpeculativeHold: (held, because) => torrentPool.setSpeculativeHold?.(held, because),
|
|
1215
1216
|
setPriorityMap: async ({ sourceKey, fileIndex, durationSeconds, zones }) => {
|
|
1216
|
-
void sourcePreparation?.refresh();
|
|
1217
1217
|
const record = sourceRegistry.get(sourceKey);
|
|
1218
1218
|
if (!record) {
|
|
1219
1219
|
return;
|
|
@@ -1224,9 +1224,13 @@ export async function startProxyServer({
|
|
|
1224
1224
|
if (!outputParts.viewers.forSource(sourceKey).length) probeReads.forget(sourceKey, fileIndex);
|
|
1225
1225
|
await downloadMaps.forget(sourceKey, fileIndex, { keepPreparation: true });
|
|
1226
1226
|
playbackProjections.get(`${sourceKey}:${fileIndex}`)?.clear();
|
|
1227
|
+
void sourcePreparation?.refresh();
|
|
1227
1228
|
return;
|
|
1228
1229
|
}
|
|
1229
|
-
await
|
|
1230
|
+
await publishPlaybackBeforePreparationRefresh(
|
|
1231
|
+
() => downloadMaps.playback({ sourceKey, fileIndex, durationSeconds, zones }),
|
|
1232
|
+
() => { void sourcePreparation?.refresh(); }
|
|
1233
|
+
);
|
|
1230
1234
|
mediaReads.retain(sourceKey, fileIndex, params => !params.requestId?.startsWith("download:") ||
|
|
1231
1235
|
zones.some(zone => zone.from === params.downloadInterval?.from && zone.to === params.downloadInterval?.to));
|
|
1232
1236
|
} catch (error) {
|
|
@@ -1375,6 +1379,7 @@ export async function startProxyServer({
|
|
|
1375
1379
|
const held = await torrentPool.heldRangesOf(torrent, work.fileIndex);
|
|
1376
1380
|
return work.inputRanges.every(([start, end]) => held.some(([from, to]) => from <= start && to >= end));
|
|
1377
1381
|
},
|
|
1382
|
+
playbackAppliedFor: (sourceKey, fileIndex) => downloadMaps.hasAppliedPlayback(sourceKey, fileIndex),
|
|
1378
1383
|
readyChanged: (work, ready) => logger.info(`source urgent request=${work.requestId} viewer=${work.ownerId} ` +
|
|
1379
1384
|
`source=${work.sourceKey} file=${work.fileIndex} inputReady=${ready}`),
|
|
1380
1385
|
inspect: async work => {
|
|
@@ -2219,60 +2219,78 @@ export class Renditions {
|
|
|
2219
2219
|
this.#host.outputs.touch(already);
|
|
2220
2220
|
return already;
|
|
2221
2221
|
}
|
|
2222
|
-
const
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
transcodeAudio
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
:
|
|
2274
|
-
|
|
2275
|
-
|
|
2222
|
+
const waitStartedAt = performance.now();
|
|
2223
|
+
const waitFields = {
|
|
2224
|
+
process: "P6.a",
|
|
2225
|
+
baseSessionId: base.id,
|
|
2226
|
+
trackIndex,
|
|
2227
|
+
consumerId,
|
|
2228
|
+
audioMode: transcodeAudio ? "transcode" : "copy"
|
|
2229
|
+
};
|
|
2230
|
+
this.#host.observeWait?.(base.file.sourceKey, "process", { ...waitFields, phase: "begin", at: waitStartedAt });
|
|
2231
|
+
let waitOutcome = "error";
|
|
2232
|
+
try {
|
|
2233
|
+
const rendition = await this.#host.createOrGetSession({
|
|
2234
|
+
sourceKey: base.file.sourceKey,
|
|
2235
|
+
fileIndex: base.file.fileIndex,
|
|
2236
|
+
// No picture at all: the video flag says what to do with a video stream
|
|
2237
|
+
// this output does not carry.
|
|
2238
|
+
transcodeVideo: false,
|
|
2239
|
+
transcodeAudio,
|
|
2240
|
+
fileName: base.file.name,
|
|
2241
|
+
// Opened by nobody in particular, as a step is.
|
|
2242
|
+
consumerId: "",
|
|
2243
|
+
audioTrackIndex: trackIndex,
|
|
2244
|
+
audioOnly: true,
|
|
2245
|
+
// Where the viewer is, so the rendition starts with the picture rather
|
|
2246
|
+
// than at the beginning of the file. Read the same way a quality variant
|
|
2247
|
+
// reads it: the base's own field is only written by a seek or by a
|
|
2248
|
+
// segment IT served, so on a resume-from-position open it is still unset
|
|
2249
|
+
// while the player is asking for segment #537 — and the audio would begin
|
|
2250
|
+
// at zero and never catch up, since nothing treats a far request as a
|
|
2251
|
+
// seek. The accessor falls back to the last segment actually requested.
|
|
2252
|
+
// Where the PICTURE is, not where it has been read to.
|
|
2253
|
+
//
|
|
2254
|
+
// The position this class keeps is written by the segments a session
|
|
2255
|
+
// serves, so it is the READ head, and the viewer's picture sits behind it
|
|
2256
|
+
// by everything the player has buffered. Started at the read head, the
|
|
2257
|
+
// audio run begins AHEAD of the viewer, and every request they then make
|
|
2258
|
+
// is behind a run that only moves forward — field 2026-08-15, placed at
|
|
2259
|
+
// #16 while the player asked for #10, and the audio arrived only after
|
|
2260
|
+
// the encoder was dragged back.
|
|
2261
|
+
//
|
|
2262
|
+
// The distance is measured, not assumed: the browser reports how many
|
|
2263
|
+
// seconds it holds ahead of the picture with every link report, so the
|
|
2264
|
+
// playhead is one subtraction away. A stale report is no use — a viewer
|
|
2265
|
+
// who seeked since then is somewhere else entirely — so an old one is
|
|
2266
|
+
// ignored and the whole look-ahead is subtracted instead, which cannot
|
|
2267
|
+
// leave the run ahead of them.
|
|
2268
|
+
startPositionSeconds: this.#host.audioStartSecondsFor({
|
|
2269
|
+
family: this.#host.outputs.familyOf(base),
|
|
2270
|
+
openedAtSeconds: this.#host.encodeRuns.progressOf(base)?.startPositionSeconds,
|
|
2271
|
+
segmentSeconds: this.#host.segmentDurationSec
|
|
2272
|
+
}),
|
|
2273
|
+
segmentFormatId: base.segmentFormat.id,
|
|
2274
|
+
// Cut where the picture is cut. Two streams meant to be played together
|
|
2275
|
+
// have to be divided at the same times, and the grid is the base's — the
|
|
2276
|
+
// table as it stands now, corrections included. A base on the uniform
|
|
2277
|
+
// grid passes nothing: the rendition computes the same even grid from the
|
|
2278
|
+
// same duration.
|
|
2279
|
+
inheritedGrid: base.timeline.cutGrid === "keyframe"
|
|
2280
|
+
? {
|
|
2281
|
+
boundaries: base.timeline.boundaries,
|
|
2282
|
+
published: base.timeline.published
|
|
2283
|
+
}
|
|
2284
|
+
: null
|
|
2285
|
+
});
|
|
2286
|
+
waitOutcome = rendition ? "opened" : "unavailable";
|
|
2287
|
+
return rendition ?? null;
|
|
2288
|
+
} finally {
|
|
2289
|
+
const waitEndedAt = performance.now();
|
|
2290
|
+
this.#host.observeWait?.(base.file.sourceKey, "process", { ...waitFields, phase: "end",
|
|
2291
|
+
at: waitEndedAt, startedAt: waitStartedAt, endedAt: waitEndedAt,
|
|
2292
|
+
durationMs: waitEndedAt - waitStartedAt, outcome: waitOutcome });
|
|
2293
|
+
}
|
|
2276
2294
|
}
|
|
2277
2295
|
|
|
2278
2296
|
/**
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep source preparation stated until the viewer's playback map is applied.
|
|
3
|
+
*
|
|
4
|
+
* @param {() => Promise<unknown>} publishPlayback
|
|
5
|
+
* @param {() => unknown} refreshPreparation
|
|
6
|
+
* @returns {Promise<void>}
|
|
7
|
+
*/
|
|
8
|
+
export async function publishPlaybackBeforePreparationRefresh(publishPlayback, refreshPreparation) {
|
|
9
|
+
await publishPlayback();
|
|
10
|
+
refreshPreparation();
|
|
11
|
+
}
|
|
@@ -162,6 +162,11 @@ export class DownloadMaps {
|
|
|
162
162
|
return this.#files.get(`${sourceKey}:${fileIndex}`)?.epoch ?? 0;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
hasAppliedPlayback(sourceKey, fileIndex) {
|
|
166
|
+
const file = this.#files.get(`${sourceKey}:${fileIndex}`);
|
|
167
|
+
return Boolean(file?.zones.length && file.shape && file.appliedShape === file.shape);
|
|
168
|
+
}
|
|
169
|
+
|
|
165
170
|
native({ sourceKey, fileIndex, zones }) {
|
|
166
171
|
const file = this.#file(sourceKey, fileIndex);
|
|
167
172
|
file.nativeZones = zones;
|
|
@@ -325,7 +330,12 @@ export class DownloadMaps {
|
|
|
325
330
|
const answeredAt = this.#clock();
|
|
326
331
|
// A torrent not yet in the thread applies nothing, so this map was not
|
|
327
332
|
// delivered and the next one, however alike, has to be.
|
|
328
|
-
if (reply?.appliedMs === null
|
|
333
|
+
if (reply?.appliedMs === null) {
|
|
334
|
+
if (file.shape === shape) file.shape = null;
|
|
335
|
+
if (file.appliedShape === shape) file.appliedShape = null;
|
|
336
|
+
} else {
|
|
337
|
+
file.appliedShape = shape;
|
|
338
|
+
}
|
|
329
339
|
// The window open now, which a report since this map was asked for may have started.
|
|
330
340
|
const stats = this.#statsOf(file);
|
|
331
341
|
stats.sent += 1;
|
|
@@ -345,6 +355,7 @@ export class DownloadMaps {
|
|
|
345
355
|
// sent, and leaves nothing to compare the next one against.
|
|
346
356
|
file.pending = pending.catch(() => {
|
|
347
357
|
if (file.shape === shape) file.shape = null;
|
|
358
|
+
if (file.appliedShape === shape) file.appliedShape = null;
|
|
348
359
|
});
|
|
349
360
|
return pending;
|
|
350
361
|
}
|
|
@@ -38,6 +38,7 @@ export class SourcePreparation {
|
|
|
38
38
|
#reprice;
|
|
39
39
|
#inputReady;
|
|
40
40
|
#readyChanged;
|
|
41
|
+
#playbackAppliedFor;
|
|
41
42
|
#work = new Map();
|
|
42
43
|
#revision = 0;
|
|
43
44
|
#completedRevision = null;
|
|
@@ -45,7 +46,8 @@ export class SourcePreparation {
|
|
|
45
46
|
#candidateAbort = new AbortController();
|
|
46
47
|
|
|
47
48
|
constructor({ viewers, candidatesFor, relatedFilesFor = () => [], priorityFor = (_viewer, priority) => priority,
|
|
48
|
-
inputReady = async () => false, readyChanged = () => {},
|
|
49
|
+
inputReady = async () => false, readyChanged = () => {}, playbackAppliedFor = () => true,
|
|
50
|
+
inspect, withdraw, reprice = () => {}, failed }) {
|
|
49
51
|
this.#viewers = viewers;
|
|
50
52
|
this.#candidates = candidatesFor;
|
|
51
53
|
this.#related = relatedFilesFor;
|
|
@@ -56,6 +58,7 @@ export class SourcePreparation {
|
|
|
56
58
|
this.#reprice = reprice;
|
|
57
59
|
this.#inputReady = inputReady;
|
|
58
60
|
this.#readyChanged = readyChanged;
|
|
61
|
+
this.#playbackAppliedFor = playbackAppliedFor;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
refresh() {
|
|
@@ -80,8 +83,13 @@ export class SourcePreparation {
|
|
|
80
83
|
|
|
81
84
|
/** Current owners determine priority; old work never restores a withdrawn read. */
|
|
82
85
|
demandFor(work) {
|
|
86
|
+
const handoffFileIndex = work.selected || (work.role === "source-rest" && work.ownerFileIndex === work.fileIndex)
|
|
87
|
+
? work.fileIndex : null;
|
|
88
|
+
const opening = Number.isInteger(handoffFileIndex) && this.#viewers.forSource(work.sourceKey).some(viewer =>
|
|
89
|
+
(work.ownerId === undefined || viewer.id === work.ownerId) && viewer.source.selectedFileIndex === handoffFileIndex &&
|
|
90
|
+
viewer.outputs.size > 0 && !this.#playbackAppliedFor(work.sourceKey, handoffFileIndex));
|
|
83
91
|
if (work.selected && work.statement === "packets" &&
|
|
84
|
-
work.positions !== this.#positionsFor(work.sourceKey, work.fileIndex, work.ownerId)) return null;
|
|
92
|
+
work.positions !== this.#positionsFor(work.sourceKey, work.fileIndex, work.ownerId, opening)) return null;
|
|
85
93
|
if (work.role === "subtitle-embedded" && ["packets", "subtitle-cues"].includes(work.statement) &&
|
|
86
94
|
work.positions !== this.#positionsFor(work.sourceKey, work.fileIndex, work.ownerId, true)) return null;
|
|
87
95
|
const owners = this.#viewers.forSource(work.sourceKey).filter(viewer => {
|
|
@@ -90,9 +98,9 @@ export class SourcePreparation {
|
|
|
90
98
|
viewer.subtitle.trackIndex !== work.trackIndex)) return false;
|
|
91
99
|
const selected = viewer.source.selectedFileIndex;
|
|
92
100
|
if (work.ownerFileIndex !== undefined) return selected === work.ownerFileIndex &&
|
|
93
|
-
(work.role !== "source-rest" || viewer.outputs.size === 0);
|
|
101
|
+
(work.role !== "source-rest" || viewer.outputs.size === 0 || opening);
|
|
94
102
|
if (work.statement !== "packets") return selected === null || selected === work.fileIndex;
|
|
95
|
-
if (selected !== null) return selected === work.fileIndex && viewer.outputs.size === 0 && work.selected;
|
|
103
|
+
if (selected !== null) return selected === work.fileIndex && (viewer.outputs.size === 0 || opening) && work.selected;
|
|
96
104
|
return !work.selected && viewer.source.visibleFileIndices.includes(work.fileIndex);
|
|
97
105
|
});
|
|
98
106
|
if (!owners.length) return null;
|