@torrent-tv/proxy 2.87.14 → 2.87.16

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.
@@ -0,0 +1,72 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
4
+ import { readPresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
5
+
6
+ function box(type, body) {
7
+ const header = Buffer.alloc(8);
8
+ header.writeUInt32BE(body.length + 8);
9
+ header.write(type, 4);
10
+ return Buffer.concat([header, body]);
11
+ }
12
+
13
+ function init(emptyMs, mediaTime) {
14
+ const mvhd = Buffer.alloc(24);
15
+ mvhd.writeUInt32BE(1000, 12);
16
+ const tkhd = Buffer.alloc(20);
17
+ tkhd.writeUInt32BE(1, 12);
18
+ const mdhd = Buffer.alloc(24);
19
+ mdhd.writeUInt32BE(24000, 12);
20
+ const elst = Buffer.alloc(32);
21
+ elst.writeUInt32BE(2, 4);
22
+ elst.writeUInt32BE(emptyMs, 8);
23
+ elst.writeInt32BE(-1, 12);
24
+ elst.writeUInt32BE(1000, 20);
25
+ elst.writeInt32BE(mediaTime, 24);
26
+ return box("moov", Buffer.concat([box("mvhd", mvhd), box("trak", Buffer.concat([
27
+ box("tkhd", tkhd), box("edts", box("elst", elst)), box("mdia", box("mdhd", mdhd))
28
+ ]))]));
29
+ }
30
+
31
+ function fragment(decodeTime, composition) {
32
+ const tfhd = Buffer.alloc(12);
33
+ tfhd.writeUIntBE(8, 1, 3);
34
+ tfhd.writeUInt32BE(1, 4);
35
+ tfhd.writeUInt32BE(1000, 8);
36
+ const tfdt = Buffer.alloc(12);
37
+ tfdt[0] = 1;
38
+ tfdt.writeBigUInt64BE(BigInt(decodeTime), 4);
39
+ const trun = Buffer.alloc(12);
40
+ trun.writeUIntBE(0x800, 1, 3);
41
+ trun.writeUInt32BE(1, 4);
42
+ trun.writeUInt32BE(composition, 8);
43
+ return box("moof", box("traf", Buffer.concat([
44
+ box("tfhd", tfhd), box("tfdt", tfdt), box("trun", trun)
45
+ ])));
46
+ }
47
+
48
+ test("a shared init preserves presentation times without adding composition delay twice", () => {
49
+ const shared = fmp4Format.prepareSharedInit(init(83, 2000));
50
+ const own = init(4125, 3000);
51
+ const media = Buffer.concat([fragment(0, 3000), fragment(48000, 3000)]);
52
+ const raw = Buffer.concat([own, media]);
53
+ const prepared = fmp4Format.prepareSegmentBytes(media, {
54
+ startSeconds: 4.125, initBytes: shared, rawBytes: raw
55
+ });
56
+ assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
57
+ const times = [];
58
+ walkBoxes(prepared, (type, start) => {
59
+ if (type === "tfdt") times.push(Number(prepared.readBigUInt64BE(start + 4)));
60
+ });
61
+ assert.deepEqual(times, [98000, 146000]);
62
+ });
63
+
64
+ test("a shared init first read after a seek also supports returning to the beginning", () => {
65
+ const shared = fmp4Format.prepareSharedInit(init(304083, 2000));
66
+ const media = fragment(0, 2000);
67
+ const raw = Buffer.concat([init(83, 2000), media]);
68
+ const prepared = fmp4Format.prepareSegmentBytes(media, {
69
+ startSeconds: 0.083, initBytes: shared, rawBytes: raw
70
+ });
71
+ assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
72
+ });
@@ -18,6 +18,26 @@ import { PieceDiskStore } from "../services/storage/piece-store/piece-disk-store
18
18
 
19
19
  const PIECE = 4096;
20
20
 
21
+ test("bytes held before a memory wait cannot be forgotten or evicted", async () => {
22
+ const { store } = await makeStore(PIECE);
23
+ try {
24
+ await store.write(0, Buffer.alloc(PIECE, 7));
25
+ const release = store.hold(0);
26
+ store.forget(0);
27
+ store.forgetBehind([1]);
28
+ await store.write(1, Buffer.alloc(PIECE, 8));
29
+ const target = Buffer.alloc(PIECE);
30
+ assert.equal(await store.read(0, target), PIECE);
31
+ assert.equal(target[0], 7);
32
+ release();
33
+ release();
34
+ store.forget(0);
35
+ assert.equal(store.has(0), false);
36
+ } finally {
37
+ await store.destroy();
38
+ }
39
+ });
40
+
21
41
  /**
22
42
  * @param {number | null} allowanceBytes
23
43
  * @returns {Promise<{ store: PieceDiskStore, directory: string, clock: { at: number } }>}
@@ -22,6 +22,7 @@ const CHUNK = 1024;
22
22
  */
23
23
  function makeDisk({ failWrite = false, failRead = false, holdWrites = false } = {}) {
24
24
  const stored = new Map();
25
+ const reading = new Map();
25
26
  /** @type {Array<() => void>} */
26
27
  const held = [];
27
28
  return {
@@ -61,6 +62,15 @@ function makeDisk({ failWrite = false, failRead = false, holdWrites = false } =
61
62
  has(index) {
62
63
  return stored.has(index);
63
64
  },
65
+ hold(index) {
66
+ if (!stored.has(index)) return null;
67
+ reading.set(index, (reading.get(index) ?? 0) + 1);
68
+ return () => {
69
+ const count = reading.get(index) - 1;
70
+ if (count > 0) reading.set(index, count);
71
+ else reading.delete(index);
72
+ };
73
+ },
64
74
  async write(index, bytes) {
65
75
  if (holdWrites) {
66
76
  await new Promise((resolve) => held.push(resolve));
@@ -82,7 +92,7 @@ function makeDisk({ failWrite = false, failRead = false, holdWrites = false } =
82
92
  return bytes.length;
83
93
  },
84
94
  forget(index) {
85
- stored.delete(index);
95
+ if (!reading.has(index)) stored.delete(index);
86
96
  },
87
97
  async close() {},
88
98
  async destroy() {
@@ -123,6 +133,21 @@ const get = (store, index) =>
123
133
  store.get(index, undefined, (error, bytes) => (error ? reject(error) : resolve(bytes)))
124
134
  );
125
135
 
136
+ test("a revival holds disk bytes across the asynchronous memory reservation", async () => {
137
+ const { store, disk } = makeStore();
138
+ disk.stored.set(0, piece(0));
139
+ try {
140
+ const revival = store.reside(0);
141
+ disk.forget(0);
142
+ const located = await revival;
143
+ assert.deepEqual(Buffer.from(located.buffer, located.offset, located.length), piece(0));
144
+ disk.forget(0);
145
+ assert.equal(disk.has(0), false);
146
+ } finally {
147
+ await new Promise((resolve) => store.destroy(resolve));
148
+ }
149
+ });
150
+
126
151
  /**
127
152
  * Wait until a condition holds, rather than for a chosen interval — a test that
128
153
  * sleeps samples, it does not check (roadmap item 54).
@@ -107,12 +107,14 @@ test("does not produce an ETA without a client-link measurement", () => {
107
107
  assert.equal(forecast.reason, "link-rate-unavailable");
108
108
  });
109
109
 
110
- test("rate trend projects the measured linear trend without a fitted weight", () => {
110
+ test("rate forecast does not extrapolate acceleration beyond observed service", () => {
111
111
  const trend = new RateTrend();
112
112
  trend.add(9_000, 2);
113
113
  trend.add(10_000, 4);
114
114
 
115
- assert.equal(forecastRate(trend.snapshot(), 10_000, 2), 6);
115
+ const predicted = forecastRate(trend.snapshot(), 10_000, 2);
116
+ assert.ok(predicted > 3 && predicted < 4);
117
+ assert.ok(forecastRate(trend.snapshot(), 100_000, 2) >= 3);
116
118
  });
117
119
 
118
120
  test("uses each track's own segment boundaries and requires continuous coverage on both tracks", () => {
@@ -331,7 +333,7 @@ test("shares one torrent download rate across distinct video and audio files", (
331
333
  assert.ok(Math.abs(forecast.delaySeconds - 4.032) < 1e-8);
332
334
  });
333
335
 
334
- test("finds a safe interval when rising encode speed and falling link speed make the forecast non-monotonic", () => {
336
+ test("does not invent future encode acceleration to start an unsustainable output", () => {
335
337
  const now = 1_000;
336
338
  const forecast = predictPlaybackReadiness({
337
339
  now,
@@ -360,7 +362,81 @@ test("finds a safe interval when rising encode speed and falling link speed make
360
362
  });
361
363
 
362
364
  assert.equal(forecast.ready, false);
363
- assert.equal(forecast.reason, "minimum-safe-delay");
364
- assert.ok(Number.isFinite(forecast.delaySeconds));
365
- assert.ok(forecast.delaySeconds > 4 && forecast.delaySeconds < 5);
365
+ assert.equal(forecast.reason, "no-safe-start-found");
366
+ assert.equal(forecast.delaySeconds, null);
367
+ });
368
+
369
+ test("a faster old probe cannot turn positive delivery into a permanent zero rate", () => {
370
+ const trend = new RateTrend(120);
371
+ trend.add(0, 112_400_000);
372
+ trend.add(66_757, 37_640_000);
373
+ assert.equal(forecastRate(trend.snapshot(), 20 * 60_000, 1443.97), 37_640_000);
374
+ assert.equal(trend.add(66_757, 37_640_000), false);
375
+ });
376
+
377
+ test("the measurement window retains one boundary sample and discards older history", () => {
378
+ const trend = new RateTrend(2);
379
+ trend.add(0, 100);
380
+ trend.add(1_000, 40);
381
+ trend.add(4_000, 20);
382
+ assert.equal(trend.snapshot().count, 2);
383
+ assert.equal(trend.snapshot().meanValue, 30);
384
+ });
385
+
386
+ function preparedEpisode(bufferLimitSeconds, linkReadings) {
387
+ const durationSeconds = 1443.97;
388
+ const boundaries = [0, 10.01, 18.977, 29.488];
389
+ for (let index = 4; index <= 197; index += 1) {
390
+ boundaries.push(29.488 + (durationSeconds - 29.488) * (index - 3) / 194);
391
+ }
392
+ const segments = boundaries.slice(0, -1).map((startSeconds, index) => ({
393
+ index, startSeconds, endSeconds: boundaries[index + 1]
394
+ }));
395
+ const tracks = [9_257_176.8, 189_000].map((bitsPerMediaSecond, index) => ({
396
+ id: String(index),
397
+ sourceIds: ["source"],
398
+ processedSeconds: 1437.269,
399
+ bitsPerMediaSecond,
400
+ readings: [],
401
+ segments,
402
+ readySegmentIndices: segments.map(({ index }) => index),
403
+ segmentSizesBytes: new Map(segments.map((segment) => [segment.index,
404
+ (segment.endSeconds - segment.startSeconds) * bitsPerMediaSecond / 8]))
405
+ }));
406
+ return {
407
+ now: 20 * 60_000,
408
+ positionSeconds: 0,
409
+ durationSeconds,
410
+ bufferedAheadSeconds: 19.187,
411
+ bufferLimitSeconds,
412
+ reserveSeconds: 5.674,
413
+ lookaheadSeconds: 120,
414
+ requiredAudio: true,
415
+ sources: [{ id: "source", complete: false, bytesPerMediaSecond: 1, readings: [{ at: 0, value: 0 }] }],
416
+ tracks,
417
+ linkReadings
418
+ };
419
+ }
420
+
421
+ test("starts a prepared episode despite an old declining link measurement", () => {
422
+ const forecast = predictPlaybackReadiness(preparedEpisode(120, [
423
+ { at: 0, value: 112_400_000 }, { at: 66_757, value: 37_640_000 }
424
+ ]));
425
+ assert.equal(forecast.preparedSegments, 394);
426
+ assert.equal(forecast.ready, true);
427
+ assert.equal(forecast.reason, "trajectory-safe-now");
428
+ });
429
+
430
+ test("the measured buffer proves capacity even when the loader reports a smaller target", () => {
431
+ const forecast = predictPlaybackReadiness(preparedEpisode(9.092, [{ at: 0, value: 37_640_000 }]));
432
+ assert.equal(forecast.ready, true);
433
+ });
434
+
435
+ test("a measured source stop is not replaced by hypothetical future recovery", () => {
436
+ const forecast = predictPlaybackReadiness(input({ sources: [{
437
+ id: "source", complete: false, bytesPerMediaSecond: 1,
438
+ readings: [{ at: 9_000, value: 10 }, { at: 10_000, value: 0 }]
439
+ }] }));
440
+ assert.equal(forecast.ready, false);
441
+ assert.equal(forecast.delaySeconds, null);
366
442
  });
@@ -0,0 +1,99 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { EventEmitter } from "node:events";
4
+ import { EncodeRun } from "../services/encode/EncodeRun.js";
5
+ import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
6
+ import { predictPlaybackReadiness } from "../services/server/playback-readiness.js";
7
+
8
+ function encode() {
9
+ const child = new EventEmitter();
10
+ child.stdio = [null, new EventEmitter(), new EventEmitter(), new EventEmitter()];
11
+ child.stderr = child.stdio[2];
12
+ child.kill = () => {};
13
+ const published = [];
14
+ const ended = [];
15
+ const run = new EncodeRun({
16
+ address: "fake", from: 0, to: 1, encoder: new SoftwareEncoder(),
17
+ buildArgs: () => [], spawn: () => child, logger: { info() {}, warn() {} },
18
+ indexOfName: (name) => Number(name),
19
+ onClosed: (name) => { published.push(name); return name; },
20
+ onEnded: (value) => ended.push(value)
21
+ });
22
+ return { child, run, published, ended };
23
+ }
24
+
25
+ test("a clean short input never publishes its flushed tail", () => {
26
+ const { child, published, ended } = encode();
27
+ child.stderr.emit("data", "Stream ends prematurely at 1048576");
28
+ child.stdio[3].emit("data", "0\n");
29
+ child.emit("close", 0, null);
30
+ assert.deepEqual(published, []);
31
+ assert.equal(ended[0].ending, "short");
32
+ assert.equal(ended[0].reached, -1);
33
+ });
34
+
35
+ test("completed cuts survive a failure but its last flushed file does not", () => {
36
+ const { child, published } = encode();
37
+ child.stdio[3].emit("data", "0\n");
38
+ child.stderr.emit("data", "Stream ends prematurely at 1048576");
39
+ child.stdio[3].emit("data", "1\n");
40
+ child.emit("close", 0, null);
41
+ assert.deepEqual(published, ["0"]);
42
+ });
43
+
44
+ test("normal completion drains the filename pipe before publishing the tail", () => {
45
+ const { child, published, ended } = encode();
46
+ child.emit("exit", 0, null);
47
+ assert.deepEqual(ended, []);
48
+ child.stdio[3].emit("data", "0\n1\n");
49
+ child.emit("close", 0, null);
50
+ assert.deepEqual(published, ["0", "1"]);
51
+ assert.equal(ended[0].ending, "complete");
52
+ });
53
+
54
+ test("stopping retains an earlier closed cut and rejects the signal-flushed tail", () => {
55
+ const { child, run, published } = encode();
56
+ child.stdio[3].emit("data", "0\n");
57
+ run.stop("a different range is needed");
58
+ child.stdio[3].emit("data", "1\n");
59
+ child.emit("close", null, "SIGTERM");
60
+ assert.deepEqual(published, ["0"]);
61
+ });
62
+
63
+ function state(clientRanges) {
64
+ return {
65
+ now: 10_000, positionSeconds: 0, durationSeconds: 8,
66
+ bufferedAheadSeconds: 8, bufferLimitSeconds: 8, reserveSeconds: 0, lookaheadSeconds: 8,
67
+ sources: [{ id: "source", complete: true }],
68
+ linkReadings: [{ at: 10_000, value: 80 }],
69
+ tracks: [{ id: "video", sourceIds: ["source"], processedSeconds: 8,
70
+ bitsPerMediaSecond: 80, readings: [{ at: 10_000, value: 2 }],
71
+ readySegmentIndices: [0, 1], segmentSizesBytes: new Map([[0, 40], [1, 40]]),
72
+ clientRanges, segments: [{ index: 0, startSeconds: 0, endSeconds: 4 },
73
+ { index: 1, startSeconds: 4, endSeconds: 8 }] }]
74
+ };
75
+ }
76
+
77
+ test("future browser ranges are retained rather than transferred again", () => {
78
+ const forecast = predictPlaybackReadiness(state([{ start: 4, end: 8 }]));
79
+ assert.equal(forecast.ready, false);
80
+ assert.equal(forecast.reason, "minimum-safe-delay");
81
+ assert.ok(Math.abs(forecast.delaySeconds - 4) < 1e-8);
82
+ });
83
+
84
+ test("a real media hole is not hidden by the scalar buffer or ready file count", () => {
85
+ const input = state([{ start: 0, end: 3 }, { start: 4, end: 8 }]);
86
+ input.tracks[0].segments[0].mediaRanges = [{ start: 0, end: 3 }];
87
+ const forecast = predictPlaybackReadiness(input);
88
+ assert.equal(forecast.ready, false);
89
+ assert.equal(forecast.bufferedSeconds, 3);
90
+ assert.equal(forecast.reason, "no-safe-start-found");
91
+ });
92
+
93
+ test("missing work before global progress can still be produced", () => {
94
+ const input = state([]);
95
+ input.tracks[0].readySegmentIndices = [1];
96
+ const forecast = predictPlaybackReadiness(input);
97
+ assert.equal(forecast.reason, "minimum-safe-delay");
98
+ assert.ok(Number.isFinite(forecast.delaySeconds));
99
+ });
@@ -113,7 +113,7 @@ test("a piece still being written is not a piece, and its name says so", (t) =>
113
113
  assert.equal(store.pathOf(KEY, 1), null);
114
114
 
115
115
  // And closing it is one rename, after which it is servable.
116
- assert.equal(store.publish(KEY, "making-0-00001.mp4", fmp4Format), "segment-00001.mp4");
116
+ assert.equal(store.publish(KEY, "making-0-00001.mp4", { ...fmp4Format, readMediaRanges: undefined }), "segment-00001.mp4");
117
117
  assert.equal(store.isClosed(KEY, 1), true);
118
118
  });
119
119