@torrent-tv/proxy 2.30.0 → 2.30.2
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 +9 -0
- package/package.json +1 -1
- package/services/hls-session-manager.js +53 -23
- package/services/torrent-worker/fastest-wires.js +9 -2
- package/services/torrent-worker/piece-reader.js +5 -2
- package/test/fastest-wires.test.js +1 -1
- package/test/stale-request-after-seek.test.js +183 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 2.30.2
|
|
2
|
+
|
|
3
|
+
- **Fix**: The cut-time shift of 2.28.0 is reverted — the field measured it and it moved the cuts OFF the source's keyframes rather than onto them. Of 75 pieces the picture produced afterwards, only **nine** began at a time the container's own table names, against **70 of 75** for the soundtrack, which the change never touched; the median distance from the playlist went from 0.04 s to 4.33 s. Before it, every piece began exactly on a named keyframe and it was the playlist that disagreed with them — which is the correction path's business, not the cut list's. The reasoning that produced the shift (that the muxer decides its cuts before the output is relabelled) was argued from ffmpeg's semantics rather than measured, and the measurement says otherwise.
|
|
4
|
+
- **Fix**: The steering line compared two different things. `steered onto N of M holders` summed the successes over every attempt of a wait while taking M from the last attempt alone, which is how the log came to read `steered onto 12 of 6 holders`. Both halves are now totals over the same attempts: `steered onto N of M asks (K peers held it)`.
|
|
5
|
+
|
|
6
|
+
## 2.30.1
|
|
7
|
+
|
|
8
|
+
- **Fix**: A seek was undone a second after it was made. Measured 2026-08-17: the viewer jumped to 2083.4 s, both runs restarted at segment #373 — correctly — and then a request for #371, issued by the player BEFORE the jump and reissued a second later, dragged the encoder back to #370. The viewer sat at #374 waiting for it to return. Two things let that happen, and both are fixed. The behind-head repair refuses a request that is behind the position the VIEWER themselves reported: its existing guard only holds while a seek is still settling, which by then it was not. And a segment request may no longer move the recorded viewer position BACKWARDS past a reported seek — playback only ever moves forward from one, so nothing legitimate is lost, while a stale request can no longer rewrite the viewer's own statement, which is how the repair came to believe it. A reported seek is the viewer stating where they are; a request is evidence about where the player is reading, and evidence may refine a statement forward, never contradict it backwards. Pinned by `test/stale-request-after-seek.test.js`, whose control case shows the same traffic still repairing a genuinely misplaced run when the viewer has said nothing.
|
|
9
|
+
|
|
1
10
|
## 2.30.0
|
|
2
11
|
|
|
3
12
|
- **New**: The speed a step must sustain, and the smallest buffer that hides an interruption, are now COMPUTED from the supply's own behaviour instead of being chosen by hand — printed first, used later. A step producing at `v` gains `v - 1` seconds of cushion per second and an interruption of `W` seconds costs `W`, so it survives its own supply only while `(v - 1) × T > W`, that is `v > 1 + W / T`, with `W` the worst recent wait for a piece and `T` the median interval between such waits. On the field torrent of 2026-08-17 that is **2.42x**, against the 1.5 assumed today and the 1.05 measured on the step that stalled; on the same file's copied stream it is 1.31 against 8x measured, which is why a copy never stalls. The buffer follows from the same readings: one whole segment — the one being played — plus the worst interruption that can arrive before it refills, whichever source it comes from, which was **7-9 s** where the browser waits for 25. Both figures are logged per file every half minute, so the next session says whether the arithmetic describes reality BEFORE anything is decided by it. The arithmetic is a pure module with the field session's own numbers as its tests (`services/supply-margin.js`).
|
package/package.json
CHANGED
|
@@ -3567,30 +3567,22 @@ export class HlsSessionManager {
|
|
|
3567
3567
|
const gridCutTimes = explicitTimes && (!session.transcodeVideo || session.cutGrid === "keyframe")
|
|
3568
3568
|
? segmentCutTimesFrom(session.segmentBoundaries, safeIndex)
|
|
3569
3569
|
: null;
|
|
3570
|
-
//
|
|
3571
|
-
// timestamps, not against the labels we ask it to write. That branch keeps
|
|
3572
|
-
// the source's timestamps (`-copyts`) and re-labels the output 0-based with
|
|
3573
|
-
// `-output_ts_offset -sourceStartTime`; the cut list, being applied before
|
|
3574
|
-
// that relabelling, must therefore be stated in the SOURCE's terms.
|
|
3570
|
+
// Cut times are stated on the grid, for both branches.
|
|
3575
3571
|
//
|
|
3576
|
-
//
|
|
3577
|
-
//
|
|
3578
|
-
//
|
|
3579
|
-
//
|
|
3580
|
-
//
|
|
3581
|
-
//
|
|
3582
|
-
//
|
|
3583
|
-
//
|
|
3584
|
-
// playlist and the media drifted apart by a whole segment and the player
|
|
3585
|
-
// refetched what it could not place.
|
|
3572
|
+
// 2.28.0 added `sourceStartTime` to them on the copy branch, reasoning that
|
|
3573
|
+
// the muxer decides its cuts before the output is relabelled. The field
|
|
3574
|
+
// measured it the next session and the reasoning was wrong: of 75 pieces
|
|
3575
|
+
// the picture produced, only NINE began at a time the container's own
|
|
3576
|
+
// keyframe table names (the soundtrack, untouched by the change, scored 70
|
|
3577
|
+
// of 75). Before it, every piece began exactly on a named keyframe and it
|
|
3578
|
+
// was the PLAYLIST that disagreed with them. So the shift moved the cuts
|
|
3579
|
+
// OFF the keyframes rather than onto them, and it is gone.
|
|
3586
3580
|
//
|
|
3587
|
-
//
|
|
3588
|
-
//
|
|
3589
|
-
//
|
|
3590
|
-
//
|
|
3591
|
-
const cutTimes = gridCutTimes
|
|
3592
|
-
? gridCutTimes.map((time) => Number((time + sourceStartTime).toFixed(6)))
|
|
3593
|
-
: gridCutTimes;
|
|
3581
|
+
// What remains true, and is what that measurement is really about: the
|
|
3582
|
+
// picture cuts where the source's keyframes are, and the playlist must be
|
|
3583
|
+
// built from those same times. That is the correction path's job, not the
|
|
3584
|
+
// cut list's.
|
|
3585
|
+
const cutTimes = gridCutTimes;
|
|
3594
3586
|
|
|
3595
3587
|
// A second chance for a predecessor that survived the escalation above —
|
|
3596
3588
|
// the first block is the one that does the work. Its exit is ignored
|
|
@@ -4263,6 +4255,26 @@ export class HlsSessionManager {
|
|
|
4263
4255
|
if (session.seekSettleTimer != null) {
|
|
4264
4256
|
return;
|
|
4265
4257
|
}
|
|
4258
|
+
// And it outranks it AFTERWARDS too, which is what was missing. The guard
|
|
4259
|
+
// above only holds while the settle timer is armed — a second later it is
|
|
4260
|
+
// gone, and a request the browser issued BEFORE the seek is then treated as
|
|
4261
|
+
// fresh evidence. Field 2026-08-17: a seek to 2083.4 s put both runs at
|
|
4262
|
+
// #373, a request for #371 from before it arrived a second afterwards, and
|
|
4263
|
+
// this repair moved the encoder to #370 — three segments behind the viewer,
|
|
4264
|
+
// who waited for it to come back. A request BEHIND what the viewer
|
|
4265
|
+
// themselves reported cannot be describing where they are.
|
|
4266
|
+
const reportedSeconds = Number(session.viewerReportedSeconds);
|
|
4267
|
+
if (Number.isFinite(reportedSeconds)) {
|
|
4268
|
+
const reportedIndex = this.#segmentIndexForTime(session, reportedSeconds);
|
|
4269
|
+
if (index < reportedIndex) {
|
|
4270
|
+
this.#explainHold(
|
|
4271
|
+
session,
|
|
4272
|
+
session.segmentFormat.segmentFileName(index),
|
|
4273
|
+
`it is behind #${reportedIndex}, where the viewer said they are — answered, not obeyed`
|
|
4274
|
+
);
|
|
4275
|
+
return;
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4266
4278
|
// What separates a request the viewer is waiting for from the player
|
|
4267
4279
|
// scanning the playlist is not TIME but what else it is asking for. On a
|
|
4268
4280
|
// seek hls.js fires dozens of DIFFERENT indices within half a second (field
|
|
@@ -4370,6 +4382,14 @@ export class HlsSessionManager {
|
|
|
4370
4382
|
// The browser holds one session id for the whole file and knows nothing of
|
|
4371
4383
|
// variants, so a seek it reports means the stream on screen.
|
|
4372
4384
|
named.viewerPositionSeconds = positionSeconds;
|
|
4385
|
+
// What the viewer SAID, kept apart from what requests imply. A request is
|
|
4386
|
+
// evidence about where the player is reading; a reported seek is the viewer
|
|
4387
|
+
// stating where they are, and after one, requests already in flight
|
|
4388
|
+
// describe a place that no longer exists. Field 2026-08-17: a seek to
|
|
4389
|
+
// 2083.4 s restarted both runs at #373, a request for #371 issued before it
|
|
4390
|
+
// arrived a second later, and the encoder was dragged back to #370 — three
|
|
4391
|
+
// segments behind the viewer, who then waited for it to return.
|
|
4392
|
+
named.viewerReportedSeconds = positionSeconds;
|
|
4373
4393
|
named.lastAccessedAt = Date.now();
|
|
4374
4394
|
// The audio the viewer is listening to moves with them. It is a separate
|
|
4375
4395
|
// encoder on a separate session that the browser cannot name, and nothing
|
|
@@ -4415,6 +4435,7 @@ export class HlsSessionManager {
|
|
|
4415
4435
|
return false;
|
|
4416
4436
|
}
|
|
4417
4437
|
session.viewerPositionSeconds = positionSeconds;
|
|
4438
|
+
session.viewerReportedSeconds = positionSeconds;
|
|
4418
4439
|
session.lastAccessedAt = Date.now();
|
|
4419
4440
|
// Every segment request being held right now was made for the position the
|
|
4420
4441
|
// viewer has just left. Release them: hls.js keeps ONE fragment load
|
|
@@ -7021,7 +7042,16 @@ export class HlsSessionManager {
|
|
|
7021
7042
|
// read when a quality change has to place the next variant's first
|
|
7022
7043
|
// encode run. The freshest evidence wins: a seek overwrites this, and
|
|
7023
7044
|
// the first request after the seek overwrites it back.
|
|
7024
|
-
|
|
7045
|
+
// A request refines this only FORWARD of what the viewer reported.
|
|
7046
|
+
// Playback always moves forward from a seek, so nothing legitimate is
|
|
7047
|
+
// lost — while a stale request from before the seek can no longer
|
|
7048
|
+
// rewrite the viewer's own statement, which is what let the repair
|
|
7049
|
+
// below drag the encoder backwards.
|
|
7050
|
+
const requestedStart = this.#segmentStartTime(session, requested);
|
|
7051
|
+
const reported = Number(session.viewerReportedSeconds);
|
|
7052
|
+
if (!Number.isFinite(reported) || requestedStart >= reported) {
|
|
7053
|
+
session.viewerPositionSeconds = requestedStart;
|
|
7054
|
+
}
|
|
7025
7055
|
// A viewer who has caught up must not wait out the monitor's interval —
|
|
7026
7056
|
// but only if they HAVE caught up, which is why this re-evaluates the
|
|
7027
7057
|
// same condition instead of resuming outright.
|
|
@@ -87,7 +87,7 @@ function speedOf(wire) {
|
|
|
87
87
|
* @param {object} torrent
|
|
88
88
|
* @param {number} pieceIndex
|
|
89
89
|
* @param {number} [limit] - How many wires to push it onto.
|
|
90
|
-
* @returns {{ asked: number, considered: number, fastestBytesPerSecond: number }}
|
|
90
|
+
* @returns {{ asked: number, attempted: number, considered: number, fastestBytesPerSecond: number }}
|
|
91
91
|
* `asked` counts requests the library actually placed: it refuses when a
|
|
92
92
|
* wire's pipeline is full or when nothing can be reserved even with hotswap,
|
|
93
93
|
* and that refusal is information — a piece nobody can be asked for is
|
|
@@ -95,7 +95,7 @@ function speedOf(wire) {
|
|
|
95
95
|
*/
|
|
96
96
|
export function askFastestWiresFor(torrent, pieceIndex, limit = 3) {
|
|
97
97
|
if (!canPlaceRequests(torrent) || !Number.isInteger(pieceIndex) || pieceIndex < 0) {
|
|
98
|
-
return { asked: 0, considered: 0, fastestBytesPerSecond: 0 };
|
|
98
|
+
return { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 };
|
|
99
99
|
}
|
|
100
100
|
const candidates = wiresForPiece(torrent, pieceIndex);
|
|
101
101
|
let asked = 0;
|
|
@@ -116,6 +116,13 @@ export function askFastestWiresFor(torrent, pieceIndex, limit = 3) {
|
|
|
116
116
|
return {
|
|
117
117
|
asked,
|
|
118
118
|
considered: candidates.length,
|
|
119
|
+
// How many of the asks the library placed, against how many it was asked
|
|
120
|
+
// for. The caller sums these over the whole wait, and summing `asked`
|
|
121
|
+
// against a `considered` taken from the LAST attempt is how the field log
|
|
122
|
+
// came to read "steered onto 12 of 6 holders" — a ratio of two different
|
|
123
|
+
// things. Both halves are returned per attempt so the caller can add each
|
|
124
|
+
// to its own total.
|
|
125
|
+
attempted: Math.min(candidates.length, Math.max(1, limit)),
|
|
119
126
|
fastestBytesPerSecond: candidates.length > 0 ? speedOf(candidates[0]) : 0
|
|
120
127
|
};
|
|
121
128
|
}
|
|
@@ -517,12 +517,15 @@ export async function* readFragments({
|
|
|
517
517
|
// holder delivers — measured 2026-08-17, the swarm had a fivefold surplus
|
|
518
518
|
// of bandwidth and the reader still waited 1.0-4.5 s, 47 times in two
|
|
519
519
|
// minutes, on pieces five peers already had.
|
|
520
|
-
let pushed = { asked: 0, considered: 0, fastestBytesPerSecond: 0 };
|
|
520
|
+
let pushed = { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 };
|
|
521
521
|
const pushToFastest = () => {
|
|
522
522
|
try {
|
|
523
523
|
const result = askFastestWiresFor(torrent, pieceIndex);
|
|
524
524
|
pushed = {
|
|
525
525
|
asked: pushed.asked + result.asked,
|
|
526
|
+
// Summed like the successes, so the line compares two totals over
|
|
527
|
+
// the same attempts instead of a total against a snapshot.
|
|
528
|
+
attempted: (pushed.attempted ?? 0) + result.attempted,
|
|
526
529
|
considered: result.considered,
|
|
527
530
|
fastestBytesPerSecond: result.fastestBytesPerSecond
|
|
528
531
|
};
|
|
@@ -592,7 +595,7 @@ export async function* readFragments({
|
|
|
592
595
|
// What WE did about it, so the next session says whether steering
|
|
593
596
|
// the piece onto faster holders shortens the tail — by number
|
|
594
597
|
// rather than by impression.
|
|
595
|
-
`; steered onto ${pushed.asked} of ${pushed.considered}
|
|
598
|
+
`; steered onto ${pushed.asked} of ${pushed.attempted} asks (${pushed.considered} peers held it)` +
|
|
596
599
|
(pushed.fastestBytesPerSecond > 0
|
|
597
600
|
? `, fastest ${Math.round(pushed.fastestBytesPerSecond / 1024)}KB/s`
|
|
598
601
|
: "")
|
|
@@ -97,7 +97,7 @@ test("a build without the request entry is reported, not silently skipped", () =
|
|
|
97
97
|
assert.equal(canPlaceRequests({ wires: [] }), false);
|
|
98
98
|
assert.equal(canPlaceRequests({ wires: [], _request: () => true }), true);
|
|
99
99
|
const result = askFastestWiresFor({ wires: [wire({ speed: 1 })] }, 1);
|
|
100
|
-
assert.deepEqual(result, { asked: 0, considered: 0, fastestBytesPerSecond: 0 });
|
|
100
|
+
assert.deepEqual(result, { asked: 0, attempted: 0, considered: 0, fastestBytesPerSecond: 0 });
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
test("a wire that cannot say how fast it is ranks last rather than throwing", () => {
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A request issued before a seek must not steer the encoder.
|
|
3
|
+
*
|
|
4
|
+
* Field 2026-08-17: the viewer seeked to 2083.4 s, both runs restarted at
|
|
5
|
+
* segment #373, and a request for #371 — issued before the seek and reissued by
|
|
6
|
+
* the player a second later — moved the encoder to #370. The viewer was at
|
|
7
|
+
* #374 and waited for the encoder to come back to them.
|
|
8
|
+
*
|
|
9
|
+
* The rule pinned here: a reported seek is the viewer STATING where they are; a
|
|
10
|
+
* segment request is evidence about where the player is reading. Evidence may
|
|
11
|
+
* refine a statement forward, never contradict it backwards.
|
|
12
|
+
*
|
|
13
|
+
* Both cases go through `getFileStream`, the way production reaches the repair,
|
|
14
|
+
* and the second is the control: without a reported seek the very same traffic
|
|
15
|
+
* DOES move the encoder, which is what makes the first case a measurement of
|
|
16
|
+
* the guard rather than of the weather.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import test from "node:test";
|
|
21
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
|
|
25
|
+
import { HlsSessionManager } from "../services/hls-session-manager.js";
|
|
26
|
+
import { ENCODE_RUN_STATE } from "../services/encode-run-state.js";
|
|
27
|
+
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
28
|
+
|
|
29
|
+
const SEGMENT_SECONDS = 4;
|
|
30
|
+
const RUN_STARTS_AT = 373;
|
|
31
|
+
const BEHIND_INDEX = 371;
|
|
32
|
+
const SESSION_ID = "22222222-3333-4444-5555-666666666666";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A live session whose run begins at #373 and whose directory is empty, so any
|
|
36
|
+
* segment request is a request for something not yet produced.
|
|
37
|
+
*
|
|
38
|
+
* @returns {Promise<{ manager: HlsSessionManager, session: object, dirPath: string }>}
|
|
39
|
+
*/
|
|
40
|
+
async function sessionWithRunAt373() {
|
|
41
|
+
const dirPath = await mkdtemp(path.join(os.tmpdir(), "stale-seek-"));
|
|
42
|
+
const manager = new HlsSessionManager({
|
|
43
|
+
enabled: true,
|
|
44
|
+
ffmpegBin: "ffmpeg",
|
|
45
|
+
localBindHost: "127.0.0.1",
|
|
46
|
+
localPort: 9090
|
|
47
|
+
});
|
|
48
|
+
const boundaries = [];
|
|
49
|
+
for (let index = 0; index <= 600; index += 1) {
|
|
50
|
+
boundaries.push(index * SEGMENT_SECONDS);
|
|
51
|
+
}
|
|
52
|
+
const session = {
|
|
53
|
+
id: SESSION_ID,
|
|
54
|
+
dirPath,
|
|
55
|
+
state: "ready",
|
|
56
|
+
runState: ENCODE_RUN_STATE.PRODUCING,
|
|
57
|
+
fileName: "film.mkv",
|
|
58
|
+
createEntryMs: Date.now(),
|
|
59
|
+
lastAccessedAt: Date.now(),
|
|
60
|
+
consumers: new Set(),
|
|
61
|
+
segmentFormat: fmp4Format,
|
|
62
|
+
usesExplicitCuts: true,
|
|
63
|
+
useSyntheticPlaylist: true,
|
|
64
|
+
playlistText: "#EXTM3U\n",
|
|
65
|
+
segmentBoundaries: boundaries,
|
|
66
|
+
segmentCount: boundaries.length - 1,
|
|
67
|
+
encodeStartIndex: RUN_STARTS_AT,
|
|
68
|
+
// A live process: the repair refuses outright when nothing is encoding.
|
|
69
|
+
ffmpeg: { pid: 1234, killed: false, exitCode: null, signalCode: null, kill() { this.killed = true; } },
|
|
70
|
+
encodeRunGeneration: 0,
|
|
71
|
+
runSerial: 1,
|
|
72
|
+
behindHeadAsks: new Map(),
|
|
73
|
+
firstWantedAt: new Map(),
|
|
74
|
+
holdExplainedAt: new Map(),
|
|
75
|
+
seekSettleTimer: null,
|
|
76
|
+
seekTarget: null,
|
|
77
|
+
seekFailureTarget: -1,
|
|
78
|
+
seekFailureCount: 0,
|
|
79
|
+
waitEpoch: 0,
|
|
80
|
+
firstSegmentLogged: true,
|
|
81
|
+
progress: { processedSeconds: RUN_STARTS_AT * SEGMENT_SECONDS, speed: "1.0x", startPositionSeconds: RUN_STARTS_AT * SEGMENT_SECONDS }
|
|
82
|
+
};
|
|
83
|
+
manager.sessionsById.set(SESSION_ID, session);
|
|
84
|
+
return { manager, session, dirPath };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The traffic that dragged the encoder back: the same index asked for twice,
|
|
89
|
+
* first wanted long enough ago to pass the repair's patience guard.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} session
|
|
92
|
+
*/
|
|
93
|
+
function askedTwiceLongEnough(session) {
|
|
94
|
+
session.firstWantedAt.set(BEHIND_INDEX, Date.now() - 5_000);
|
|
95
|
+
session.behindHeadAsks.set(BEHIND_INDEX, { count: 3, at: Date.now() });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Put the session down without going through disposal.
|
|
100
|
+
*
|
|
101
|
+
* Disposal signals the encoder and waits for it to die, which a stub cannot do
|
|
102
|
+
* — and none of that is what these tests are about. Clearing the map and any
|
|
103
|
+
* armed timer leaves nothing running.
|
|
104
|
+
*
|
|
105
|
+
* @param {HlsSessionManager} manager
|
|
106
|
+
* @param {object} session
|
|
107
|
+
* @param {string} dirPath
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
110
|
+
async function tidy(manager, session, dirPath) {
|
|
111
|
+
if (session.seekSettleTimer) {
|
|
112
|
+
clearTimeout(session.seekSettleTimer);
|
|
113
|
+
session.seekSettleTimer = null;
|
|
114
|
+
}
|
|
115
|
+
manager.sessionsById.clear();
|
|
116
|
+
manager.stop?.();
|
|
117
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test("a request behind where the viewer said they are does not move the encoder", async (t) => {
|
|
121
|
+
const { manager, session, dirPath } = await sessionWithRunAt373();
|
|
122
|
+
t.after(async () => {
|
|
123
|
+
await tidy(manager, session, dirPath);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// The viewer stated their position: 2083.4 s, which is segment #520 here.
|
|
127
|
+
manager.requestSeek(SESSION_ID, 2083.4);
|
|
128
|
+
session.encodeStartIndex = RUN_STARTS_AT;
|
|
129
|
+
askedTwiceLongEnough(session);
|
|
130
|
+
|
|
131
|
+
const answer = await manager.getFileStream(
|
|
132
|
+
SESSION_ID,
|
|
133
|
+
fmp4Format.segmentFileName(BEHIND_INDEX),
|
|
134
|
+
{ requestSeq: 1 }
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
assert.equal(answer.kind, "warming-up", "the request is answered, not obeyed");
|
|
138
|
+
// The viewer's own seek legitimately armed a move to #519. What must NOT
|
|
139
|
+
// happen is the stale request replacing that with #370 — which is exactly
|
|
140
|
+
// what the field log shows: `seek settle → restart at segment #370`.
|
|
141
|
+
assert.notEqual(
|
|
142
|
+
session.seekTarget,
|
|
143
|
+
BEHIND_INDEX - 1,
|
|
144
|
+
"a request behind the viewer must not become the encoder's destination"
|
|
145
|
+
);
|
|
146
|
+
assert.equal(session.seekTarget, 519, "the viewer's own seek is what the encoder is going to");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("the same traffic DOES move the encoder when the viewer has said nothing", async (t) => {
|
|
150
|
+
const { manager, session, dirPath } = await sessionWithRunAt373();
|
|
151
|
+
t.after(async () => {
|
|
152
|
+
await tidy(manager, session, dirPath);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// No reported seek: a run placed wrongly is exactly what the repair is for,
|
|
156
|
+
// and this is the case it must keep serving.
|
|
157
|
+
askedTwiceLongEnough(session);
|
|
158
|
+
|
|
159
|
+
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(BEHIND_INDEX), { requestSeq: 1 });
|
|
160
|
+
|
|
161
|
+
assert.equal(
|
|
162
|
+
session.seekTarget,
|
|
163
|
+
BEHIND_INDEX - 1,
|
|
164
|
+
"with nothing said by the viewer, a request stuck behind the head still repairs the run"
|
|
165
|
+
);
|
|
166
|
+
assert.notEqual(session.seekSettleTimer, null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a request cannot move the viewer's position backwards", async (t) => {
|
|
170
|
+
const { manager, session, dirPath } = await sessionWithRunAt373();
|
|
171
|
+
t.after(async () => {
|
|
172
|
+
await tidy(manager, session, dirPath);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
manager.requestSeek(SESSION_ID, 2083.4);
|
|
176
|
+
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(BEHIND_INDEX), { requestSeq: 1 });
|
|
177
|
+
|
|
178
|
+
assert.equal(
|
|
179
|
+
session.viewerPositionSeconds,
|
|
180
|
+
2083.4,
|
|
181
|
+
"a stale request must not rewrite what the viewer reported — that is how the repair came to believe it"
|
|
182
|
+
);
|
|
183
|
+
});
|