@torrent-tv/proxy 2.95.10 → 2.95.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.95.10",
3
+ "version": "2.95.12",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -986,7 +986,7 @@ export class EncodeRuns {
986
986
  if (Number.isInteger(index) && (index < safeIndex || index > run.to)) return null;
987
987
  let bytes = null;
988
988
  if (admittedInput && session.segmentFormat.id === "fmp4" && session.spec.carries !== "audio-only" &&
989
- session.spec.audio && !this.#host.servesAudioSeparately(session)) {
989
+ (admittedInput.original || session.spec.audio && !this.#host.servesAudioSeparately(session))) {
990
990
  try {
991
991
  const address = session.outputKey ?? "";
992
992
  const grid = session.timeline.published ?? session.timeline.boundaries;
@@ -9,7 +9,7 @@ export function presentationSegment(current, following, { from, to }) {
9
9
  const edit = own.edits.get(id);
10
10
  if (!edit) throw new Error("A segment track has no declared clock.");
11
11
  const samples = [...track.samples];
12
- if (track.kind === "soun" && next?.tracks.has(id)) {
12
+ if (next?.tracks.has(id)) {
13
13
  const nextEdit = next.edits.get(id);
14
14
  if (!nextEdit || nextEdit.timescale !== edit.timescale) throw new Error("Adjacent segment clocks differ.");
15
15
  for (const sample of next.tracks.get(id).samples) {
@@ -18,18 +18,33 @@ export function presentationSegment(current, following, { from, to }) {
18
18
  }
19
19
  const start = BigInt(Math.round(from * Number(edit.timescale)));
20
20
  const end = BigInt(Math.round(to * Number(edit.timescale)));
21
- const selected = samples.filter(sample => {
21
+ const wanted = samples.filter(sample => {
22
22
  const pts = sample.dts + sample.composition + edit.offset;
23
- return pts < end && pts + sample.duration > start;
23
+ return pts < end && (track.kind === "vide" ? pts >= start : pts + sample.duration > start);
24
24
  });
25
+ if (!wanted.length) throw new Error("A presentation segment lacks required media.");
26
+ // An open-GOP picture before the cut can follow the next CRA in decode
27
+ // order. Retain the reference pictures through its last required DTS;
28
+ // dropping the CRA would make the recovered leading pictures undecodable.
29
+ const lastDecode = wanted.reduce((last, sample) => sample.dts > last ? sample.dts : last, -1n);
30
+ const firstDecode = wanted.reduce((first, sample) => first === null || sample.dts < first ? sample.dts : first, null);
31
+ const selected = track.kind === "vide" ? samples.filter(sample =>
32
+ sample.dts >= firstDecode && sample.dts <= lastDecode && sample.dts + sample.composition + edit.offset >= start) : wanted;
25
33
  selected.sort((left, right) => left.dts < right.dts ? -1 : left.dts > right.dts ? 1 : 0);
26
- if (!selected.length) throw new Error("A presentation segment lacks required media.");
27
34
  tracks.push({ id, samples: selected });
28
35
  }
29
36
  const makeMoof = offset => box("moof", full("mfhd", 0, 0, u32(1)), ...tracks.map(track => {
30
- const entries = track.samples.map(sample => Buffer.concat([
31
- u32(Number(sample.duration)), u32(sample.bytes.length), u32(sample.flags), i32(Number(sample.composition))
32
- ]));
37
+ let decodeTime = track.samples[0].dts;
38
+ const entries = track.samples.map(sample => {
39
+ // Removing leading pictures can leave a gap in decode timestamps.
40
+ // trun advances by durations only; retain each original presentation
41
+ // timestamp relative to the decode clock actually written here.
42
+ const composition = sample.dts + sample.composition - decodeTime;
43
+ decodeTime += sample.duration;
44
+ return Buffer.concat([
45
+ u32(Number(sample.duration)), u32(sample.bytes.length), u32(sample.flags), i32(Number(composition))
46
+ ]);
47
+ });
33
48
  const payload = Buffer.concat([u32(track.samples.length), i32(offset), ...entries]);
34
49
  offset += track.samples.reduce((bytes, sample) => bytes + sample.bytes.length, 0);
35
50
  const clock = Buffer.alloc(8);
@@ -58,17 +58,26 @@ import os from "node:os";
58
58
  * Version 1 states the file lists of the cgroup alone as `active_file` and of
59
59
  * it with its descendants as `total_active_file`; its usage includes the
60
60
  * descendants, so the hierarchical figures are the ones that subtract from it.
61
+ *
62
+ * Version 2 has two limits. `memory.max` is where the kernel kills;
63
+ * `memory.high` is where it throttles the cgroup and pushes its memory out —
64
+ * on the Home Assistant kernel a 300 MB allocation took 2123 ms instead of
65
+ * 527 ms past a 128 MiB `memory.high`, with 186 MiB of it sent to swap
66
+ * (torrent-tv/meta#155). systemd's `MemoryHigh=` sets it without
67
+ * `MemoryMax=`, so it is a limit of its own, and a level's limit is the
68
+ * smaller of the two. Version 1's soft limit is not a counterpart: it only
69
+ * orders reclaim when the whole machine is short.
61
70
  */
62
71
  const HIERARCHIES = {
63
72
  1: {
64
73
  mount: "/sys/fs/cgroup/memory",
65
- limit: "memory.limit_in_bytes",
74
+ limits: ["memory.limit_in_bytes"],
66
75
  usage: "memory.usage_in_bytes",
67
76
  reclaimable: ["total_active_file", "total_inactive_file"]
68
77
  },
69
78
  2: {
70
79
  mount: "/sys/fs/cgroup",
71
- limit: "memory.max",
80
+ limits: ["memory.max", "memory.high"],
72
81
  usage: "memory.current",
73
82
  reclaimable: ["active_file", "inactive_file", "slab_reclaimable"]
74
83
  }
@@ -230,18 +239,24 @@ function levelsOf(mount, path) {
230
239
  * @param {string} directory
231
240
  * @param {number} hostTotal
232
241
  * @returns {{ roomBytes: number, limitBytes: number } | null} Null where this
233
- * level states no limit — `max`, the version 1 sentinel — or is not there.
242
+ * level states no limit — `max` in every limit file, the version 1
243
+ * sentinel — or is not there.
234
244
  */
235
245
  function roomAt(readFile, hierarchy, directory, hostTotal) {
236
- let limitBytes;
237
246
  let usageBytes;
238
247
  try {
239
- limitBytes = bytesOf(readFile(`${directory}/${hierarchy.limit}`));
240
248
  usageBytes = bytesOf(readFile(`${directory}/${hierarchy.usage}`));
241
249
  } catch {
242
250
  return null; // silent-ok: no such level, or no memory controller on it.
243
251
  }
244
- if (!Number.isFinite(limitBytes) || limitBytes >= hostTotal || !Number.isFinite(usageBytes)) {
252
+ const limitBytes = Math.min(...hierarchy.limits.map((name) => {
253
+ try {
254
+ return bytesOf(readFile(`${directory}/${name}`));
255
+ } catch {
256
+ return Number.NaN; // silent-ok: a limit file this kernel does not have states no limit.
257
+ }
258
+ }).filter(Number.isFinite));
259
+ if (limitBytes >= hostTotal || !Number.isFinite(usageBytes)) {
245
260
  return null;
246
261
  }
247
262
  let reclaimableBytes = 0;