@torrent-tv/proxy 2.87.16 → 2.87.17

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 CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.87.17
2
+
3
+ - **Fix**: Measure re-encoded HLS fragment intervals against their separate init and served timestamp position, without caching an absent header as empty media.
4
+
1
5
  ## 2.87.16
2
6
 
3
7
  - **Fix**: Hold spilled source bytes across memory reservation so concurrent eviction cannot terminate ffmpeg input with a missing disk piece.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.87.16",
3
+ "version": "2.87.17",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -279,7 +279,12 @@ export const fmp4Format = {
279
279
  * transcode itself; the box walk never descends into `mdat`.
280
280
  */
281
281
  needsSegmentRewrite: true,
282
- readMediaRanges: readPresentationRanges,
282
+ readMediaRanges(raw, { initBytes = null, startSeconds = 0 } = {}) {
283
+ if (this.extractInit(raw)) return readPresentationRanges(raw);
284
+ if (!initBytes?.length) return undefined;
285
+ const served = this.prepareSegmentBytes(raw, { initBytes, startSeconds, rawBytes: raw });
286
+ return readPresentationRanges(Buffer.concat([initBytes, served]));
287
+ },
283
288
 
284
289
  /**
285
290
  * Whether a segment carries every track the init promises.
@@ -49,6 +49,9 @@ import { mpegtsFormat } from "./mpegts.js";
49
49
  * @property {(bytes: Buffer, context: PrepareSegmentContext) => Buffer}
50
50
  * prepareSegmentBytes - Correct a segment before serving. Identity for
51
51
  * formats that need nothing.
52
+ * @property {(bytes: Buffer, context?: Partial<PrepareSegmentContext>) =>
53
+ * { start: number, end: number }[] | undefined} [readMediaRanges] - Actual
54
+ * served presentation intervals, or undefined until their header is available.
52
55
  */
53
56
 
54
57
  /** @type {Readonly<Record<string, SegmentFormat>>} */
@@ -569,7 +569,9 @@ export class ViewerRequests {
569
569
  index,
570
570
  startSeconds: timeline.publishedStartOf(index),
571
571
  endSeconds: timeline.publishedStartOf(index + 1),
572
- mediaRanges: this.#host.segmentStore.mediaRangesOf(output.outputKey, index)
572
+ mediaRanges: this.#host.segmentStore.mediaRangesOf(output.outputKey, index, {
573
+ startSeconds: timeline.publishedStartOf(index)
574
+ })
573
575
  }));
574
576
  const sourceIndexes = [output.spec.video?.fileIndex, output.spec.audio?.fileIndex]
575
577
  .filter((fileIndex) => Number.isInteger(fileIndex) && fileIndex >= 0);
@@ -471,7 +471,7 @@ export class SegmentStore {
471
471
  return served;
472
472
  }
473
473
 
474
- mediaRangesOf(key, index) {
474
+ mediaRangesOf(key, index, { startSeconds = 0 } = {}) {
475
475
  const known = this.#mediaRanges.get(key)?.get(index);
476
476
  if (known) return known;
477
477
  const format = this.#formats.get(key);
@@ -479,7 +479,16 @@ export class SegmentStore {
479
479
  const filePath = this.pathOf(key, index);
480
480
  if (!filePath) return undefined;
481
481
  try {
482
- const ranges = format.readMediaRanges(readFileSync(filePath));
482
+ let initBytes = this.initOf(key);
483
+ if (!initBytes && format.initFileName) {
484
+ try {
485
+ initBytes = readFileSync(path.join(this.pathFor(key), format.initFileName));
486
+ } catch {
487
+ // A header not yet produced is not an empty media interval.
488
+ }
489
+ }
490
+ const ranges = format.readMediaRanges(readFileSync(filePath), { initBytes, startSeconds });
491
+ if (!ranges) return undefined;
483
492
  const byIndex = this.#mediaRanges.get(key) ?? new Map();
484
493
  byIndex.set(index, ranges);
485
494
  this.#mediaRanges.set(key, byIndex);
@@ -2,6 +2,10 @@ import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
4
4
  import { readPresentationRanges, walkBoxes } from "../services/encode/segment-formats/mp4-boxes.js";
5
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
5
9
 
6
10
  function box(type, body) {
7
11
  const header = Buffer.alloc(8);
@@ -70,3 +74,24 @@ test("a shared init first read after a seek also supports returning to the begin
70
74
  });
71
75
  assert.deepEqual(readPresentationRanges(Buffer.concat([shared, prepared])), readPresentationRanges(raw));
72
76
  });
77
+
78
+ test("HLS fragments use the separate init and the same timeline translation as serving", (t) => {
79
+ const root = mkdtempSync(path.join(os.tmpdir(), "presentation-ranges-"));
80
+ const store = new SegmentStore({ root });
81
+ t.after(() => {
82
+ store.dropAll("the check is over");
83
+ rmSync(root, { recursive: true, force: true });
84
+ });
85
+ const key = "encoded-video";
86
+ store.useFormat(key, fmp4Format);
87
+ const dir = store.directoryFor(key);
88
+ const media = fragment(0, 2000);
89
+ writeFileSync(path.join(dir, fmp4Format.segmentFileName(1)), media);
90
+ assert.equal(store.mediaRangesOf(key, 1, { startSeconds: 4 }), undefined);
91
+ const header = init(0, 0);
92
+ writeFileSync(path.join(dir, fmp4Format.initFileName), header);
93
+ const served = fmp4Format.prepareSegmentBytes(media, { initBytes: header, startSeconds: 4 });
94
+ assert.deepEqual(store.mediaRangesOf(key, 1, { startSeconds: 4 }),
95
+ readPresentationRanges(Buffer.concat([header, served])));
96
+ assert.ok(store.mediaRangesOf(key, 1)[0].start > 4);
97
+ });