@torrent-tv/proxy 2.107.23 → 2.107.25
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/server.js +3 -4
- package/services/encode/EncodeRuns.js +1 -4
- package/services/encode/output/Timeline.js +8 -38
- package/services/server/ViewerRequests.js +4 -1
- package/services/torrent/PlaybackProjection.js +11 -12
- package/services/viewer/playback-readiness.js +10 -5
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -73,7 +73,6 @@ import { nativeSourceMap, nativeOriginalSourceMap } from "./services/viewer/Nati
|
|
|
73
73
|
import { MediaReadRequests } from "./services/media/MediaReadRequests.js";
|
|
74
74
|
import { SegmentInputs } from "./services/media/SegmentInputs.js";
|
|
75
75
|
import { directSegmentInput } from "./services/media/direct-segment-input.js";
|
|
76
|
-
import { piecesRunPastEnd } from "./services/encode/output/Timeline.js";
|
|
77
76
|
import { ContainerTrack } from "./services/media/tracks/ContainerTrack.js";
|
|
78
77
|
import { pauseCoefficient, viewerStartsOn } from "./services/viewer/PriorityMap.js";
|
|
79
78
|
import { coalescing } from "./utils/coalesce.js";
|
|
@@ -717,9 +716,9 @@ export async function startProxyServer({
|
|
|
717
716
|
const revision = outputParts.segmentStore.revisionOf(output.outputKey);
|
|
718
717
|
const links = consumers.map(viewer => viewer.linkReading()?.linkMbps)
|
|
719
718
|
.filter(value => Number.isFinite(value) && value > 0);
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
played.push({ outputKey: output.outputKey, grid,
|
|
719
|
+
// Every piece answers for its exact ending boundary, as the encode
|
|
720
|
+
// plan reads it: the player may request the preceding piece there.
|
|
721
|
+
played.push({ outputKey: output.outputKey, grid,
|
|
723
722
|
encodeSpeed: outputParts.encodeCost.latestSpeedReadingOf(output)?.speed,
|
|
724
723
|
bytesPerSecond: bytesPerSecondOf(output, revision), linkMbps: links.length ? Math.min(...links) : null,
|
|
725
724
|
readyRevision: revision, ready: () => outputParts.segmentStore.sizesOf(output.outputKey),
|
|
@@ -26,7 +26,6 @@ import { cutOf, cutShiftOf, judgeNeighbors, judgePiece, leadingSecondsOf, lacksO
|
|
|
26
26
|
import { leadingPictures, presentationSegment } from "./segment-formats/presentation-segment.js";
|
|
27
27
|
import { InputFailures, failedAdmittedInput } from "./InputFailures.js";
|
|
28
28
|
import { readProcessCpuSeconds } from "./host-load.js";
|
|
29
|
-
import { piecesRunPastEnd } from "./output/Timeline.js";
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
/**
|
|
@@ -616,13 +615,11 @@ export class EncodeRuns {
|
|
|
616
615
|
// each: 454 pieces against 401 on the field file.
|
|
617
616
|
for (const [address, sessions] of byOutput) {
|
|
618
617
|
const timeline = sessions[0].timeline;
|
|
619
|
-
const spec = sessions[0].spec;
|
|
620
618
|
this.#host.encodeOrchestrator.notePriorityMap(
|
|
621
619
|
address,
|
|
622
620
|
timeline?.inSegments?.(
|
|
623
621
|
this.#host.priority.mapForOutput(address),
|
|
624
|
-
Number(timeline?.segmentCount) || 0
|
|
625
|
-
{ runsPastEnd: piecesRunPastEnd(spec) }
|
|
622
|
+
Number(timeline?.segmentCount) || 0
|
|
626
623
|
) ?? []
|
|
627
624
|
);
|
|
628
625
|
}
|
|
@@ -42,32 +42,6 @@
|
|
|
42
42
|
* reveal where the file's cuts really are.
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* Whether an output's pieces may hold a little past their published end: a
|
|
47
|
-
* piece ends at the first frame boundary at or after it, and the player places
|
|
48
|
-
* the next piece where this one truly ends (hls.js `updateFragPTSDTS`). A
|
|
49
|
-
* viewer standing at a published edge is then asked for by the piece BEFORE
|
|
50
|
-
* it. True of a copied picture, whose pieces end at a packet, and of sound
|
|
51
|
-
* however it is made: copied sound ends at a packet, and encoded sound at a
|
|
52
|
-
* frame of the encoder — 1024 samples of AAC, 21.3 ms at 48 kHz. Only a
|
|
53
|
-
* re-encoded picture, given a keyframe at every cut, ends where it is published.
|
|
54
|
-
*
|
|
55
|
-
* Field 2026-10-09, MP3 copied out of an AVI: a seek to 4000 s asked for the
|
|
56
|
-
* piece ending at 4000 s. Field r43 (Home Assistant, 2026-10-15), a copied
|
|
57
|
-
* Matroska picture: a seek to 1000.000 s, the published start of #96, asked for
|
|
58
|
-
* #95 on both tracks; the picture's pieces were not counted as copies, nothing
|
|
59
|
-
* made #95, and the picture stood 131 s until the viewer moved. With the
|
|
60
|
-
* picture counted (r44), its #95 was made in 6.4 s and the soundtrack's, AAC
|
|
61
|
-
* encoded from the film (`its muxer counts cuts -0.021333s`), was still not
|
|
62
|
-
* made (torrent-tv/meta#178).
|
|
63
|
-
*
|
|
64
|
-
* @param {{ video?: object | null, audio?: object | null, transcodesVideo?: boolean, transcodesAudio?: boolean } | null | undefined} spec
|
|
65
|
-
* @returns {boolean}
|
|
66
|
-
*/
|
|
67
|
-
export function piecesRunPastEnd(spec) {
|
|
68
|
-
return (Boolean(spec?.video) && spec.transcodesVideo !== true) || Boolean(spec?.audio);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
45
|
export class Timeline {
|
|
72
46
|
/**
|
|
73
47
|
* @param {object} params
|
|
@@ -185,13 +159,10 @@ export class Timeline {
|
|
|
185
159
|
* @param {import("../../viewer/PriorityMap.js").PriorityMap} map - What every
|
|
186
160
|
* viewer of this output wants, in seconds of film.
|
|
187
161
|
* @param {number} segmentCount - How many pieces this output has.
|
|
188
|
-
* @param {{ runsPastEnd?: boolean }} [shape] - `runsPastEnd`: the output's
|
|
189
|
-
* pieces copy a stream, which is cut where its packets end, so a piece
|
|
190
|
-
* holds sound past its published end by up to one packet.
|
|
191
162
|
* @returns {import("../../viewer/PriorityMap.js").DemandZone[]} Runs of pieces
|
|
192
163
|
* that agree. Empty where nothing is stated, which says nobody is coming.
|
|
193
164
|
*/
|
|
194
|
-
inSegments(map, segmentCount
|
|
165
|
+
inSegments(map, segmentCount) {
|
|
195
166
|
if (!(segmentCount > 0) || !map || !(map.durationSeconds > 0)) {
|
|
196
167
|
return [];
|
|
197
168
|
}
|
|
@@ -202,17 +173,16 @@ export class Timeline {
|
|
|
202
173
|
// whole, so it is wanted as soon as the soonest second inside it is
|
|
203
174
|
// wanted — and the piece a viewer is standing in the middle of is wanted
|
|
204
175
|
// exactly as much as the second under their feet.
|
|
205
|
-
// A
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
// 4000 s, which nothing made because second 4000 belonged to the next.
|
|
176
|
+
// A PIECE ALSO HOLDS THE SECOND ITS END FALLS IN. At an exact playlist
|
|
177
|
+
// boundary hls.js may request the fragment ending there before it asks
|
|
178
|
+
// for the one beginning there. Excluding that second from this piece can
|
|
179
|
+
// leave the request waiting while the plan starts at the following one.
|
|
180
|
+
// Copied streams also run past their published end by up to one packet,
|
|
181
|
+
// and hls.js carries that parsed end to every later piece.
|
|
212
182
|
const from = Math.max(0, Math.floor(this.publishedStartOf(index)));
|
|
213
183
|
const end = this.publishedStartOf(index + 1);
|
|
214
184
|
const until = index + 1 < segmentCount
|
|
215
|
-
? Math.max(from + 1,
|
|
185
|
+
? Math.max(from + 1, Math.floor(end) + 1)
|
|
216
186
|
: map.durationSeconds;
|
|
217
187
|
// Asked of the map for the piece's seconds as a whole, not second by
|
|
218
188
|
// second: a walk of every second of the film for every output on every
|
|
@@ -1282,8 +1282,11 @@ export class ViewerRequests {
|
|
|
1282
1282
|
const ranges = inventory.has(index) ? this.#host.encodeRuns.finishedMediaRangesOf(output, index) : undefined;
|
|
1283
1283
|
const mediaRanges = ranges && output.segmentFormat?.servedMediaRanges ?
|
|
1284
1284
|
output.segmentFormat.servedMediaRanges(ranges, { initBytes: sessionInit }) : undefined;
|
|
1285
|
+
// At a playlist boundary the player can request the fragment ending
|
|
1286
|
+
// exactly at its seek target. It is work needed to reach the target,
|
|
1287
|
+
// even though it adds no media after the viewer's position.
|
|
1285
1288
|
if (index !== 0 && mediaRanges === undefined && !inventory.has(index) &&
|
|
1286
|
-
Number.isFinite(endSeconds) && Number.isFinite(positionSeconds) && endSeconds
|
|
1289
|
+
Number.isFinite(endSeconds) && Number.isFinite(positionSeconds) && endSeconds < positionSeconds) {
|
|
1287
1290
|
continue;
|
|
1288
1291
|
}
|
|
1289
1292
|
const startSeconds = timeline.publishedStartOf(index);
|
|
@@ -273,10 +273,9 @@ class Segment {
|
|
|
273
273
|
this.from = from;
|
|
274
274
|
this.to = to;
|
|
275
275
|
/**
|
|
276
|
-
* Up to which second the map's zones are read for it: its end
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* standing at its end for this piece, and its bytes are fetched for them.
|
|
276
|
+
* Up to which second the map's zones are read for it: its end and the exact
|
|
277
|
+
* end boundary. The encode plan includes that boundary for every output,
|
|
278
|
+
* since the player may request the piece ending there before the next one.
|
|
280
279
|
*/
|
|
281
280
|
this.reach = reach;
|
|
282
281
|
/** Whether a zone of the map overlaps it. */
|
|
@@ -311,16 +310,15 @@ class Segment {
|
|
|
311
310
|
|
|
312
311
|
/** One output: its grid, how long its segments take to make and deliver, what is made, and its next input. */
|
|
313
312
|
class OutputState {
|
|
314
|
-
constructor(outputKey, grid
|
|
313
|
+
constructor(outputKey, grid) {
|
|
315
314
|
this.outputKey = outputKey;
|
|
316
315
|
this.grid = grid;
|
|
317
|
-
this.runsPastEnd = runsPastEnd;
|
|
318
316
|
/** @type {Segment[]} */
|
|
319
317
|
this.segments = [];
|
|
320
318
|
for (let index = 0; index + 1 < grid.length; index++) {
|
|
321
319
|
const end = grid[index + 1];
|
|
322
320
|
this.segments.push(new Segment(this, index, grid[index], end,
|
|
323
|
-
|
|
321
|
+
index + 2 < grid.length ? Math.max(end, Math.floor(end) + 1) : end));
|
|
324
322
|
}
|
|
325
323
|
this.encodeSpeed = undefined;
|
|
326
324
|
this.bytesPerSecond = undefined;
|
|
@@ -380,7 +378,7 @@ export class PlaybackProjection {
|
|
|
380
378
|
* @param {object} params
|
|
381
379
|
* @param {Array<{ from: number, to: number, priority: number, urgent?: boolean, behind?: boolean, deferred?: boolean,
|
|
382
380
|
* deadlineAt?: number }>} params.zones - The file's map zones, deadlines as instants.
|
|
383
|
-
* @param {Array<{ outputKey: string, grid: number[],
|
|
381
|
+
* @param {Array<{ outputKey: string, grid: number[], encodeSpeed?: number, bytesPerSecond?: number | null,
|
|
384
382
|
* linkMbps?: number | null, readyRevision: unknown, ready: () => Map<number, number>,
|
|
385
383
|
* target: number | null }>} params.outputs - Every output of the file somebody is watching, with
|
|
386
384
|
* what decides how long its segments take to make and deliver (`sourceDeadline`), what it has made
|
|
@@ -401,13 +399,13 @@ export class PlaybackProjection {
|
|
|
401
399
|
for (const info of outputs) {
|
|
402
400
|
seen.add(info.outputKey);
|
|
403
401
|
let output = this.#outputs.get(info.outputKey);
|
|
404
|
-
if (output &&
|
|
402
|
+
if (output && output.grid !== info.grid) {
|
|
405
403
|
this.#dropOutput(output);
|
|
406
404
|
output = undefined;
|
|
407
405
|
}
|
|
408
406
|
const fresh = !output;
|
|
409
407
|
if (fresh) {
|
|
410
|
-
output = new OutputState(info.outputKey, info.grid
|
|
408
|
+
output = new OutputState(info.outputKey, info.grid);
|
|
411
409
|
this.#outputs.set(info.outputKey, output);
|
|
412
410
|
}
|
|
413
411
|
// How long a segment of it takes to make and deliver.
|
|
@@ -436,8 +434,9 @@ export class PlaybackProjection {
|
|
|
436
434
|
for (const segment of output.segments) if (this.#demand(segment, zones, ordered, nextBase, leadMoved)) reworked++;
|
|
437
435
|
} else {
|
|
438
436
|
for (const { from, to } of stretches) {
|
|
439
|
-
// A
|
|
440
|
-
|
|
437
|
+
// A viewer at an exact playlist boundary may request the piece
|
|
438
|
+
// ending there before requesting the one that starts there.
|
|
439
|
+
for (let index = Math.max(0, segmentAt(output.grid, from) - 1);
|
|
441
440
|
index < output.segments.length && output.segments[index].from < to; index++) {
|
|
442
441
|
if (this.#demand(output.segments[index], zones, ordered, nextBase, false)) reworked++;
|
|
443
442
|
}
|
|
@@ -223,7 +223,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
223
223
|
bitsPerMediaSecond: averageBitsPerMediaSecond(track, track.segments,
|
|
224
224
|
new Set(Array.isArray(track.readySegmentIndices) ? track.readySegmentIndices : [])),
|
|
225
225
|
segments: segmentsOf[trackIndex]
|
|
226
|
-
.filter((segment) => isPiece(segment) && segment.endSeconds
|
|
226
|
+
.filter((segment) => isPiece(segment) && segment.endSeconds >= position && segment.startSeconds < horizonEnd)
|
|
227
227
|
.sort((left, right) => left.startSeconds - right.startSeconds || left.index - right.index),
|
|
228
228
|
// The pieces after the near window, as the playlist cuts them, and the
|
|
229
229
|
// offset the page's player placed them by.
|
|
@@ -290,9 +290,12 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
290
290
|
for (const segment of segments) {
|
|
291
291
|
// Media the page already holds is not transferred again: every range of
|
|
292
292
|
// the piece still ahead of the viewer lies inside what the page holds.
|
|
293
|
+
const exactBoundaryPrerequisite = segment.endSeconds === position;
|
|
293
294
|
let held = segment.ranges.length > 0;
|
|
294
295
|
for (const range of segment.ranges) {
|
|
295
|
-
|
|
296
|
+
const requiredStart = exactBoundaryPrerequisite ? range.start : latest(range.start, at);
|
|
297
|
+
if ((exactBoundaryPrerequisite || compare(range.end, at) > 0) &&
|
|
298
|
+
!contains(state.clientRanges, requiredStart, range.end)) {
|
|
296
299
|
held = false;
|
|
297
300
|
break;
|
|
298
301
|
}
|
|
@@ -876,8 +879,10 @@ function withSeekStartTolerance(ranges, position, unit) {
|
|
|
876
879
|
|
|
877
880
|
/**
|
|
878
881
|
* The pieces of a track this forecast places in exact time: those still to
|
|
879
|
-
* play (ending after `position`) that start before `horizonEnd`, the first
|
|
880
|
-
* piece, every piece that has been read and every piece that is made.
|
|
882
|
+
* play (ending at or after `position`) that start before `horizonEnd`, the first
|
|
883
|
+
* piece, every piece that has been read and every piece that is made. The piece
|
|
884
|
+
* ending exactly at a seek position is kept as a prerequisite: the playlist
|
|
885
|
+
* player can request it before the piece starting at that position. A piece
|
|
881
886
|
* that is none of these changes no exact answer: after the near window it is
|
|
882
887
|
* scheduled in seconds, it is not where the track starts, no unread piece is
|
|
883
888
|
* placed by it (only read pieces are), it lends no frame to what the page
|
|
@@ -891,7 +896,7 @@ function withSeekStartTolerance(ranges, position, unit) {
|
|
|
891
896
|
function piecesThatMatter(track, position, horizonEnd = Number.POSITIVE_INFINITY) {
|
|
892
897
|
const made = new Set(Array.isArray(track.readySegmentIndices) ? track.readySegmentIndices : []);
|
|
893
898
|
const kept = track.segments.filter((segment) => segment?.index === 0 || segment?.mediaRanges !== undefined ||
|
|
894
|
-
made.has(segment?.index) || (!(Number.isFinite(segment?.endSeconds) && segment.endSeconds
|
|
899
|
+
made.has(segment?.index) || (!(Number.isFinite(segment?.endSeconds) && segment.endSeconds < position) &&
|
|
895
900
|
!(Number.isFinite(segment?.startSeconds) && segment.startSeconds >= horizonEnd)));
|
|
896
901
|
return kept.length === track.segments.length ? track.segments : kept;
|
|
897
902
|
}
|