@torrent-tv/proxy 2.96.2 → 2.96.3

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.96.2",
3
+ "version": "2.96.3",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -258,6 +258,19 @@ export class OutputSpec {
258
258
  return this.audio?.fileIndex ?? -1;
259
259
  }
260
260
 
261
+ /**
262
+ * The files of the torrent whose bytes this output reads: the picture's file
263
+ * where it carries a picture, the soundtrack's where it carries sound. A
264
+ * soundtrack shipped beside the picture is a file of its own, and its bytes
265
+ * are wanted from that file and not from the picture's.
266
+ *
267
+ * @returns {number[]}
268
+ */
269
+ get sourceFileIndexes() {
270
+ return [...new Set([this.video?.fileIndex, this.audio?.fileIndex]
271
+ .filter((fileIndex) => Number.isInteger(fileIndex) && fileIndex >= 0))];
272
+ }
273
+
261
274
  /** @returns {number} */
262
275
  get audioSourceTrackIndex() {
263
276
  return this.audio?.trackIndex ?? 0;
@@ -584,8 +584,7 @@ export class ViewerRequests {
584
584
  output.segmentFormat.servedMediaRanges(ranges, { initBytes: sessionInit }) : undefined
585
585
  };
586
586
  });
587
- const sourceIndexes = [output.spec.video?.fileIndex, output.spec.audio?.fileIndex]
588
- .filter((fileIndex) => Number.isInteger(fileIndex) && fileIndex >= 0);
587
+ const sourceIndexes = [...output.spec.sourceFileIndexes];
589
588
  if (sourceIndexes.length === 0) {
590
589
  sourceIndexes.push(output.file.fileIndex);
591
590
  }
@@ -216,23 +216,34 @@ export class PriorityOrchestrator {
216
216
  if (typeof sourceKey !== "string" || !sourceKey || !Number.isSafeInteger(fileIndex) || fileIndex < 0) {
217
217
  throw new TypeError("A priority map requires its output file's source and file index.");
218
218
  }
219
- const key = `${sourceKey}:${fileIndex}`;
220
219
  const durationSeconds = Number(session.file?.durationSeconds) || 0;
221
220
  // The first band is as wide as an interruption this file has actually
222
221
  // shown on this swarm, never a chosen number.
223
222
  const allowanceSeconds = this.#allowanceFor(session);
224
- let held = byFile.get(key);
225
- if (!held) {
226
- held = {
227
- sourceKey,
228
- fileIndex,
229
- durationSeconds,
230
- allowanceSeconds,
231
- viewers: [],
232
- demandKey: []
233
- };
234
- byFile.set(key, held);
235
- }
223
+ // EVERY FILE THE OUTPUT READS, not only the one it is named after. A
224
+ // soundtrack shipped beside the picture is an output of the picture's
225
+ // film whose bytes live in another file; mapped under the picture's
226
+ // file only, nobody asked the swarm for that file, its encoder waited
227
+ // for pieces for ever and the viewer who chose the soundtrack stood
228
+ // still (Home Assistant 2026-10-09, torrent-tv/meta#8).
229
+ const read = session.spec?.sourceFileIndexes;
230
+ const files = Array.isArray(read) && read.length ? read : [fileIndex];
231
+ const helds = files.map((index) => {
232
+ const fileKey = `${sourceKey}:${index}`;
233
+ let held = byFile.get(fileKey);
234
+ if (!held) {
235
+ held = {
236
+ sourceKey,
237
+ fileIndex: index,
238
+ durationSeconds,
239
+ allowanceSeconds,
240
+ viewers: [],
241
+ demandKey: []
242
+ };
243
+ byFile.set(fileKey, held);
244
+ }
245
+ return held;
246
+ });
236
247
  // Every output anybody holds a session for, whether or not a viewer is
237
248
  // consuming it — an output with nobody on it must get a map with
238
249
  // nothing in it, which is how the plan is told to stop its encoders.
@@ -240,16 +251,19 @@ export class PriorityOrchestrator {
240
251
  const address = session.outputKey ?? "";
241
252
  let mine = byOutput.get(address);
242
253
  if (!mine) {
243
- mine = { durationSeconds, allowanceSeconds, viewers: [], fileKey: key };
254
+ // Counted over a file the output itself reads, which is always among
255
+ // the files mapped above; the picture's file is not one for a
256
+ // soundtrack read from a file of its own.
257
+ mine = { durationSeconds, allowanceSeconds, viewers: [], fileKey: `${sourceKey}:${files[0]}` };
244
258
  byOutput.set(address, mine);
245
259
  }
246
260
  for (const viewer of this.#viewers.forOutput(session).values()) {
247
261
  if (!viewer.isPresent()) {
248
262
  continue;
249
263
  }
250
- held.demandKey.push([session.outputKey, viewer.id, viewer.audio?.trackIndex,
264
+ const demand = [session.outputKey, viewer.id, viewer.audio?.trackIndex,
251
265
  viewer.audio?.transcode, viewer.activeVariantId, viewer.warmingVariantId,
252
- viewer.warmingAudioId, this.#watchedBy(session, viewer)]);
266
+ viewer.warmingAudioId, this.#watchedBy(session, viewer)];
253
267
  const stated = {
254
268
  id: viewer.id,
255
269
  atSeconds: viewer.positionSeconds(now) ?? 0,
@@ -267,7 +281,10 @@ export class PriorityOrchestrator {
267
281
  ? viewer.wantsFilmNow()
268
282
  : viewer.playing === true || viewer.waiting === true)
269
283
  };
270
- held.viewers.push(stated);
284
+ for (const held of helds) {
285
+ held.demandKey.push(demand);
286
+ held.viewers.push(stated);
287
+ }
271
288
  if (this.#watchedBy(session, viewer)) {
272
289
  mine.viewers.push(stated);
273
290
  }