@torrent-tv/proxy 2.87.13 → 2.87.14
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/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/services/server/playback-readiness.js +100 -39
- package/test/playback-readiness.test.js +47 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 2.87.14
|
|
2
|
+
|
|
3
|
+
- **Fix**: Evaluate source-stall coverage against the continuous timeline already prepared on the proxy, while independently checking client-buffer continuity and delivery capacity.
|
|
4
|
+
|
|
1
5
|
## 2.87.13
|
|
2
6
|
|
|
3
7
|
- **Fix**: Include distinct active piece reads in memory demand and preserve their slots immediately, so reads without priority-map ranges cannot deadlock the torrent piece store.
|
package/package.json
CHANGED
|
@@ -81,7 +81,7 @@ export class RateTrend {
|
|
|
81
81
|
* @param {number} input.durationSeconds
|
|
82
82
|
* @param {number} input.bufferedAheadSeconds
|
|
83
83
|
* @param {number} input.bufferLimitSeconds
|
|
84
|
-
* @param {number} input.reserveSeconds
|
|
84
|
+
* @param {number} input.reserveSeconds - Measured source-stall coverage, in media seconds.
|
|
85
85
|
* @param {number} input.lookaheadSeconds
|
|
86
86
|
* @param {number} input.now
|
|
87
87
|
* @param {boolean} [input.requiredAudio]
|
|
@@ -110,12 +110,9 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
110
110
|
if (remaining <= buffered) {
|
|
111
111
|
return result(true, 0, buffered, reserve, 0, "client-buffer-covers-end", 0);
|
|
112
112
|
}
|
|
113
|
-
if (!(duration > 0) || !(capacity > 0) ||
|
|
113
|
+
if (!(duration > 0) || !(capacity > 0) || tracks.length === 0) {
|
|
114
114
|
return result(false, null, buffered, reserve, null, "incomplete-state", 0);
|
|
115
115
|
}
|
|
116
|
-
if (reserve > capacity) {
|
|
117
|
-
return result(false, null, buffered, reserve, null, "reserve-exceeds-client-capacity", 0);
|
|
118
|
-
}
|
|
119
116
|
const requiredTracks = input.requiredAudio === true ? 2 : 1;
|
|
120
117
|
if (tracks.length < requiredTracks) {
|
|
121
118
|
return result(false, null, buffered, reserve, null, "separate-audio-not-observed", 0);
|
|
@@ -166,7 +163,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
166
163
|
|
|
167
164
|
for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
|
|
168
165
|
const state = trackState[trackIndex];
|
|
169
|
-
const { track,
|
|
166
|
+
const { track, segments } = state;
|
|
170
167
|
const measuredBitsPerMediaSecond = state.bitsPerMediaSecond;
|
|
171
168
|
for (const segment of segments) {
|
|
172
169
|
if (segment.endSeconds <= position + buffered) {
|
|
@@ -277,6 +274,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
277
274
|
const trackFinish = trackState.map(() => 0);
|
|
278
275
|
let transferFinish = 0;
|
|
279
276
|
const completions = [];
|
|
277
|
+
const productions = [];
|
|
280
278
|
for (const job of jobs) {
|
|
281
279
|
const { track, trackIndex, segment, ahead, sizeBits, encodeWork, sourceIntervals } = job;
|
|
282
280
|
if (prefillOnly && (ahead > prefillReach || segment.endSeconds - position > capacity)) {
|
|
@@ -322,7 +320,14 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
322
320
|
if (!job.produced) {
|
|
323
321
|
trackFinish[trackIndex] = encodedAt;
|
|
324
322
|
}
|
|
325
|
-
const producedAt = Math.max(sourceReadyAtLatest, encodedAt);
|
|
323
|
+
const producedAt = job.produced ? 0 : Math.max(sourceReadyAtLatest, encodedAt);
|
|
324
|
+
productions.push({
|
|
325
|
+
at: producedAt,
|
|
326
|
+
sourceReadyAt: sourceReadyAtLatest,
|
|
327
|
+
trackIndex,
|
|
328
|
+
segment,
|
|
329
|
+
requiresSource: sourceIntervals.size > 0
|
|
330
|
+
});
|
|
326
331
|
const roomAt = segment.endSeconds - position <= capacity
|
|
327
332
|
? 0
|
|
328
333
|
: startDelaySeconds + segment.endSeconds - position - capacity;
|
|
@@ -339,12 +344,16 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
339
344
|
? completions.filter(({ segment }) => segment.startSeconds - position <= prefillReach &&
|
|
340
345
|
segment.endSeconds - position <= capacity)
|
|
341
346
|
: completions;
|
|
342
|
-
const
|
|
347
|
+
const usefulProductions = prefillOnly
|
|
348
|
+
? productions.filter(({ segment }) => segment.startSeconds - position <= prefillReach &&
|
|
349
|
+
segment.endSeconds - position <= capacity)
|
|
350
|
+
: productions;
|
|
351
|
+
const safety = isSafeSchedule(usefulCompletions, usefulProductions, trackState, position, duration,
|
|
343
352
|
buffered, capacity, reserve, startDelaySeconds);
|
|
344
353
|
return { safe: safety.safe,
|
|
345
354
|
bufferedAtStart: safety.bufferedAtStart,
|
|
346
|
-
|
|
347
|
-
|
|
355
|
+
neededSeconds: safety.neededSeconds,
|
|
356
|
+
finishAt: usefulCompletions.reduce((latest, item) => Math.max(latest, item.at), 0) };
|
|
348
357
|
};
|
|
349
358
|
|
|
350
359
|
const prefillReach = Math.min(capacity, lookahead, remaining);
|
|
@@ -353,7 +362,8 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
353
362
|
if (!Number.isFinite(maximumUsefulDelay)) {
|
|
354
363
|
return result(false, null, buffered, reserve, null, "prefill-cannot-complete", preparedSegments);
|
|
355
364
|
}
|
|
356
|
-
|
|
365
|
+
const immediate = schedule(0);
|
|
366
|
+
if (immediate.safe) {
|
|
357
367
|
return result(true, 0, buffered, reserve, 0, "trajectory-safe-now", preparedSegments);
|
|
358
368
|
}
|
|
359
369
|
const searchLimit = Math.max(maximumUsefulDelay, rateRecoveryDelay);
|
|
@@ -396,7 +406,7 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
396
406
|
}
|
|
397
407
|
}
|
|
398
408
|
const readiness = schedule(high);
|
|
399
|
-
return result(false, high, buffered, reserve,
|
|
409
|
+
return result(false, high, buffered, reserve, immediate.neededSeconds,
|
|
400
410
|
"minimum-safe-delay", preparedSegments, readiness.bufferedAtStart);
|
|
401
411
|
}
|
|
402
412
|
|
|
@@ -508,53 +518,104 @@ function averageBitsPerMediaSecond(track, segments, ready) {
|
|
|
508
518
|
return finiteNonNegative(track.bitsPerMediaSecond);
|
|
509
519
|
}
|
|
510
520
|
|
|
511
|
-
function isSafeSchedule(completions, trackState, position, duration, buffered, capacity, reserve,
|
|
512
|
-
|
|
513
|
-
|
|
521
|
+
function isSafeSchedule(completions, productions, trackState, position, duration, buffered, capacity, reserve,
|
|
522
|
+
startDelay) {
|
|
523
|
+
// Client continuity is constrained by its buffer limit; source-stall reserve
|
|
524
|
+
// is covered by the larger union of client data and output already prepared here.
|
|
525
|
+
const clientRanges = trackState.map(() => [{ start: position, end: position + buffered }]);
|
|
526
|
+
const preparedRanges = trackState.map(() => [{ start: position, end: position + buffered }]);
|
|
527
|
+
const orderedCompletions = [...completions]
|
|
514
528
|
.filter(({ at }) => Number.isFinite(at))
|
|
515
529
|
.sort((left, right) => left.at - right.at || left.trackIndex - right.trackIndex);
|
|
530
|
+
const orderedProductions = [...productions]
|
|
531
|
+
.filter(({ at }) => Number.isFinite(at))
|
|
532
|
+
.sort((left, right) => left.at - right.at || left.trackIndex - right.trackIndex);
|
|
533
|
+
const reserveUntil = trackState.map(() => Number.NEGATIVE_INFINITY);
|
|
534
|
+
// The measured reserve applies only until this track's last required source
|
|
535
|
+
// interval has arrived; encoding and client delivery stay in the schedule.
|
|
536
|
+
for (const production of productions) {
|
|
537
|
+
if (production.requiresSource) {
|
|
538
|
+
reserveUntil[production.trackIndex] = Math.max(reserveUntil[production.trackIndex],
|
|
539
|
+
production.sourceReadyAt);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
516
542
|
let completionIndex = 0;
|
|
543
|
+
let productionIndex = 0;
|
|
517
544
|
|
|
518
545
|
const addThrough = (time) => {
|
|
519
|
-
while (
|
|
520
|
-
const { trackIndex, segment } =
|
|
521
|
-
addRange(
|
|
546
|
+
while (productionIndex < orderedProductions.length && orderedProductions[productionIndex].at <= time) {
|
|
547
|
+
const { trackIndex, segment } = orderedProductions[productionIndex];
|
|
548
|
+
addRange(preparedRanges[trackIndex], segment.startSeconds, segment.endSeconds);
|
|
549
|
+
productionIndex += 1;
|
|
550
|
+
}
|
|
551
|
+
while (completionIndex < orderedCompletions.length && orderedCompletions[completionIndex].at <= time) {
|
|
552
|
+
const { trackIndex, segment } = orderedCompletions[completionIndex];
|
|
553
|
+
addRange(clientRanges[trackIndex], segment.startSeconds, segment.endSeconds);
|
|
554
|
+
addRange(preparedRanges[trackIndex], segment.startSeconds, segment.endSeconds);
|
|
522
555
|
completionIndex += 1;
|
|
523
556
|
}
|
|
524
557
|
};
|
|
525
|
-
const safeAt = (time) => {
|
|
558
|
+
const safeAt = (time, checkClient = true) => {
|
|
526
559
|
const playerPosition = position + Math.max(0, time - startDelay);
|
|
527
|
-
|
|
560
|
+
if (playerPosition >= duration) {
|
|
561
|
+
return { safe: true, stockSafe: true, available: 0, neededSeconds: 0 };
|
|
562
|
+
}
|
|
563
|
+
const available = Math.min(capacity, ...clientRanges.map((trackRanges) =>
|
|
528
564
|
Math.max(0, contiguousEnd(trackRanges, playerPosition) - playerPosition)));
|
|
529
|
-
|
|
530
|
-
|
|
565
|
+
let neededSeconds = 0;
|
|
566
|
+
let stockSafe = true;
|
|
567
|
+
for (let trackIndex = 0; trackIndex < trackState.length; trackIndex += 1) {
|
|
568
|
+
const required = time <= reserveUntil[trackIndex]
|
|
569
|
+
? Math.min(reserve, Math.max(0, duration - playerPosition))
|
|
570
|
+
: 0;
|
|
571
|
+
const preparedAhead = Math.max(0,
|
|
572
|
+
contiguousEnd(preparedRanges[trackIndex], playerPosition) - playerPosition);
|
|
573
|
+
const deficit = Math.max(0, required - preparedAhead);
|
|
574
|
+
neededSeconds = Math.max(neededSeconds, deficit);
|
|
575
|
+
stockSafe &&= deficit === 0;
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
safe: (!checkClient || available > 0) && stockSafe,
|
|
579
|
+
stockSafe,
|
|
580
|
+
available,
|
|
581
|
+
neededSeconds
|
|
582
|
+
};
|
|
531
583
|
};
|
|
532
584
|
|
|
533
585
|
addThrough(startDelay);
|
|
534
586
|
const atStart = safeAt(startDelay);
|
|
535
587
|
if (!atStart.safe) {
|
|
536
|
-
return { safe: false, bufferedAtStart: atStart.available };
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
588
|
+
return { safe: false, bufferedAtStart: atStart.available, neededSeconds: atStart.neededSeconds };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const playbackEnd = startDelay + Math.max(0, duration - position);
|
|
592
|
+
const playbackBoundaries = [position + buffered,
|
|
593
|
+
...trackState.flatMap(({ segments }) => segments.map(({ endSeconds }) => endSeconds))]
|
|
594
|
+
.filter((boundary) => boundary > position && boundary < duration)
|
|
595
|
+
.map((boundary) => startDelay + boundary - position);
|
|
596
|
+
const sourceReadyTimes = productions
|
|
597
|
+
.filter(({ requiresSource }) => requiresSource)
|
|
598
|
+
.map(({ sourceReadyAt }) => sourceReadyAt);
|
|
599
|
+
const eventTimes = [...orderedCompletions.map(({ at }) => at),
|
|
600
|
+
...orderedProductions.map(({ at }) => at), ...playbackBoundaries, ...sourceReadyTimes]
|
|
601
|
+
.filter((time) => Number.isFinite(time) && time > startDelay && time < playbackEnd)
|
|
602
|
+
.sort((left, right) => left - right)
|
|
603
|
+
.filter((time, index, all) => index === 0 || time !== all[index - 1]);
|
|
604
|
+
|
|
605
|
+
for (const time of eventTimes) {
|
|
606
|
+
const beforeArrival = safeAt(time, false);
|
|
607
|
+
if (!beforeArrival.stockSafe) {
|
|
608
|
+
return { safe: false, bufferedAtStart: atStart.available, neededSeconds: beforeArrival.neededSeconds };
|
|
546
609
|
}
|
|
547
610
|
addThrough(time);
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
return { safe: false, bufferedAtStart: atStart.available };
|
|
552
|
-
}
|
|
611
|
+
const afterArrival = safeAt(time);
|
|
612
|
+
if (!afterArrival.safe) {
|
|
613
|
+
return { safe: false, bufferedAtStart: atStart.available, neededSeconds: afterArrival.neededSeconds };
|
|
553
614
|
}
|
|
554
615
|
}
|
|
555
616
|
|
|
556
|
-
const atEnd = safeAt(
|
|
557
|
-
return { safe: atEnd.safe, bufferedAtStart: atStart.available };
|
|
617
|
+
const atEnd = safeAt(playbackEnd);
|
|
618
|
+
return { safe: atEnd.safe, bufferedAtStart: atStart.available, neededSeconds: atStart.neededSeconds };
|
|
558
619
|
}
|
|
559
620
|
|
|
560
621
|
function addRange(ranges, start, end) {
|
|
@@ -36,7 +36,50 @@ test("finds the first safe start from segments delivered before playback", () =>
|
|
|
36
36
|
|
|
37
37
|
assert.equal(forecast.ready, false);
|
|
38
38
|
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
39
|
-
assert.ok(Math.abs(forecast.delaySeconds -
|
|
39
|
+
assert.ok(Math.abs(forecast.delaySeconds - 1) < 1e-8);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("keeps source-stall reserve in the proxy's prepared timeline, not the capped browser buffer", () => {
|
|
43
|
+
const now = 10_000;
|
|
44
|
+
const forecast = predictPlaybackReadiness({
|
|
45
|
+
now,
|
|
46
|
+
positionSeconds: 0,
|
|
47
|
+
durationSeconds: 12,
|
|
48
|
+
bufferedAheadSeconds: 2,
|
|
49
|
+
bufferLimitSeconds: 4,
|
|
50
|
+
reserveSeconds: 6,
|
|
51
|
+
lookaheadSeconds: 12,
|
|
52
|
+
sources: [{
|
|
53
|
+
id: "source",
|
|
54
|
+
complete: false,
|
|
55
|
+
bytesPerMediaSecond: 1,
|
|
56
|
+
readings: [{ at: now - 1_000, value: 2 }, { at: now, value: 2 }]
|
|
57
|
+
}],
|
|
58
|
+
tracks: [{
|
|
59
|
+
id: "video",
|
|
60
|
+
sourceIds: ["source"],
|
|
61
|
+
processedSeconds: 8,
|
|
62
|
+
bitsPerMediaSecond: 80,
|
|
63
|
+
readings: [{ at: now - 1_000, value: 2 }, { at: now, value: 2 }],
|
|
64
|
+
segments: [
|
|
65
|
+
{ index: 0, startSeconds: 0, endSeconds: 2 },
|
|
66
|
+
{ index: 1, startSeconds: 2, endSeconds: 4 },
|
|
67
|
+
{ index: 2, startSeconds: 4, endSeconds: 6 },
|
|
68
|
+
{ index: 3, startSeconds: 6, endSeconds: 8 },
|
|
69
|
+
{ index: 4, startSeconds: 8, endSeconds: 10 },
|
|
70
|
+
{ index: 5, startSeconds: 10, endSeconds: 12 }
|
|
71
|
+
],
|
|
72
|
+
readySegmentIndices: [0, 1, 2, 3],
|
|
73
|
+
segmentSizesBytes: new Map([[0, 20], [1, 20], [2, 20], [3, 20], [4, 20], [5, 20]])
|
|
74
|
+
}],
|
|
75
|
+
linkReadings: [{ at: now - 1_000, value: 80_000 }, { at: now, value: 80_000 }]
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
assert.equal(forecast.ready, true);
|
|
79
|
+
assert.equal(forecast.reason, "trajectory-safe-now");
|
|
80
|
+
assert.equal(forecast.reserveSeconds, 6);
|
|
81
|
+
assert.equal(forecast.bufferedSeconds, 2);
|
|
82
|
+
assert.equal(forecast.preparedSegments, 4);
|
|
40
83
|
});
|
|
41
84
|
|
|
42
85
|
test("overlaps source reads and encoding, then waits for the slower stage and delivery", () => {
|
|
@@ -53,7 +96,7 @@ test("overlaps source reads and encoding, then waits for the slower stage and de
|
|
|
53
96
|
const forecast = predictPlaybackReadiness(input({ sources: [source], tracks: [track] }));
|
|
54
97
|
|
|
55
98
|
assert.equal(forecast.ready, false);
|
|
56
|
-
assert.ok(Math.abs(forecast.delaySeconds -
|
|
99
|
+
assert.ok(Math.abs(forecast.delaySeconds - 2) < 1e-8);
|
|
57
100
|
});
|
|
58
101
|
|
|
59
102
|
test("does not produce an ETA without a client-link measurement", () => {
|
|
@@ -177,7 +220,7 @@ test("downloads an overlapping source interval once when two tracks use the same
|
|
|
177
220
|
|
|
178
221
|
assert.equal(forecast.ready, false);
|
|
179
222
|
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
180
|
-
assert.ok(Math.abs(forecast.delaySeconds - 2.
|
|
223
|
+
assert.ok(Math.abs(forecast.delaySeconds - 2.032) < 1e-8);
|
|
181
224
|
});
|
|
182
225
|
|
|
183
226
|
test("downloads sources on separate torrents concurrently", () => {
|
|
@@ -319,5 +362,5 @@ test("finds a safe interval when rising encode speed and falling link speed make
|
|
|
319
362
|
assert.equal(forecast.ready, false);
|
|
320
363
|
assert.equal(forecast.reason, "minimum-safe-delay");
|
|
321
364
|
assert.ok(Number.isFinite(forecast.delaySeconds));
|
|
322
|
-
assert.ok(forecast.delaySeconds >
|
|
365
|
+
assert.ok(forecast.delaySeconds > 4 && forecast.delaySeconds < 5);
|
|
323
366
|
});
|