@torrent-tv/proxy 2.99.2 → 2.99.4

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.99.2",
3
+ "version": "2.99.4",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -43,24 +43,29 @@
43
43
  */
44
44
 
45
45
  /**
46
- * Whether an output's pieces copy a stream, picture or sound, and so may hold
47
- * a little past their published end: a copy ends at the first packet boundary
48
- * at or after it, and the player places the next piece where this one truly
49
- * ends (hls.js `updateFragPTSDTS`). A viewer standing at a published edge is
50
- * then asked for by the piece BEFORE it.
46
+ * Whether an output's pieces may hold a little past their published end: a
47
+ * piece ends at the first frame boundary at or after it, and the player places
48
+ * the next piece where this one truly ends (hls.js `updateFragPTSDTS`). A
49
+ * viewer standing at a published edge is then asked for by the piece BEFORE
50
+ * it. True of a copied picture, whose pieces end at a packet, and of sound
51
+ * however it is made: copied sound ends at a packet, and encoded sound at a
52
+ * frame of the encoder — 1024 samples of AAC, 21.3 ms at 48 kHz. Only a
53
+ * re-encoded picture, given a keyframe at every cut, ends where it is published.
51
54
  *
52
55
  * Field 2026-10-09, MP3 copied out of an AVI: a seek to 4000 s asked for the
53
56
  * piece ending at 4000 s. Field r43 (Home Assistant, 2026-10-15), a copied
54
57
  * Matroska picture: a seek to 1000.000 s, the published start of #96, asked for
55
58
  * #95 on both tracks; the picture's pieces were not counted as copies, nothing
56
- * made #95, and the picture stood 131 s until the viewer moved
57
- * (torrent-tv/meta#178).
59
+ * made #95, and the picture stood 131 s until the viewer moved. With the
60
+ * picture counted (r44), its #95 was made in 6.4 s and the soundtrack's, AAC
61
+ * encoded from the film (`its muxer counts cuts -0.021333s`), was still not
62
+ * made (torrent-tv/meta#178).
58
63
  *
59
64
  * @param {{ video?: object | null, audio?: object | null, transcodesVideo?: boolean, transcodesAudio?: boolean } | null | undefined} spec
60
65
  * @returns {boolean}
61
66
  */
62
67
  export function piecesRunPastEnd(spec) {
63
- return (Boolean(spec?.video) && spec.transcodesVideo !== true) || (Boolean(spec?.audio) && spec.transcodesAudio !== true);
68
+ return (Boolean(spec?.video) && spec.transcodesVideo !== true) || Boolean(spec?.audio);
64
69
  }
65
70
 
66
71
  export class Timeline {
@@ -251,11 +251,18 @@ export function predictPlaybackReadiness(input = {}) {
251
251
  const processingCoverage = numericCoverage((Array.isArray(track.processingRanges) ? track.processingRanges : [])
252
252
  .filter(range => Number.isFinite(range?.start) && Number.isFinite(range?.end) && range.end > range.start));
253
253
  const measuredBitsPerMediaSecond = state.bitsPerMediaSecond;
254
+ const trackSources = Array.isArray(track.sourceIds) ? track.sourceIds : [];
254
255
  for (const segment of segments) {
255
- // Media the page already holds is not transferred again.
256
- const needed = segment.ranges.filter((range) => compare(range.end, at) > 0);
257
- if (segment.ranges.length > 0 && needed.every((range) =>
258
- contains(state.clientRanges, latest(range.start, at), range.end))) {
256
+ // Media the page already holds is not transferred again: every range of
257
+ // the piece still ahead of the viewer lies inside what the page holds.
258
+ let held = segment.ranges.length > 0;
259
+ for (const range of segment.ranges) {
260
+ if (compare(range.end, at) > 0 && !contains(state.clientRanges, latest(range.start, at), range.end)) {
261
+ held = false;
262
+ break;
263
+ }
264
+ }
265
+ if (held) {
259
266
  continue;
260
267
  }
261
268
  const exactBytes = sizeOf(track.segmentSizesBytes, segment.index);
@@ -275,28 +282,30 @@ export function predictPlaybackReadiness(input = {}) {
275
282
  const remainingProcessing = uncoveredIntervals({ start: workStart, end: segment.endSeconds }, processingCoverage);
276
283
  const encodeWork = produced ? 0 : remainingProcessing.reduce((sum, part) => sum + part.end - part.start, 0);
277
284
 
278
- const sourceByteIntervals = new Map();
285
+ let sourceByteIntervals = NO_SOURCE_BYTES;
279
286
  if (!produced && encodeWork > 0) {
287
+ // Each source the piece reads, in order and once: the track's own, then
288
+ // those its inputs name. Gathered with loops and no copies, because
289
+ // this runs for every piece still to play on every poll of a waiting
290
+ // viewer, and the sets and filtered lists it built were a quarter of
291
+ // the forecast's time on a two-hour film (torrent-tv/meta#178).
280
292
  const exactInputs = Array.isArray(segment.sourceInputs) ? segment.sourceInputs : null;
281
- const requiredSources = new Set([
282
- ...(Array.isArray(track.sourceIds) ? track.sourceIds : []),
283
- ...(exactInputs ?? []).map(input => input?.sourceId)
284
- ]);
285
- for (const sourceId of requiredSources) {
293
+ const count = trackSources.length + (exactInputs?.length ?? 0);
294
+ for (let place = 0; place < count; place += 1) {
295
+ const sourceId = place < trackSources.length ? trackSources[place]
296
+ : exactInputs[place - trackSources.length]?.sourceId;
297
+ if (namedBefore(sourceId, place, trackSources, exactInputs)) continue;
286
298
  const source = sourceState.get(sourceId);
287
299
  if (!source) {
288
300
  unknownReason ??= "source-measurement-unavailable";
289
301
  } else if (source.source.complete !== true) {
290
- if (exactInputs) {
291
- const inputs = exactInputs.filter(input => input?.sourceId === sourceId);
292
- const ranges = inputs.flatMap(input => Array.isArray(input.ranges) ? input.ranges : []);
293
- if (!ranges.length || inputs.some(input => !Array.isArray(input.ranges) || !input.ranges.length) ||
294
- ranges.some(range => !range || !Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start)) {
295
- unknownReason ??= "source-input-ranges-unavailable";
296
- } else sourceByteIntervals.set(sourceId, ranges);
297
- continue;
302
+ const ranges = exactInputs ? inputRangesOf(exactInputs, sourceId) : null;
303
+ if (ranges === null) {
304
+ unknownReason ??= "source-input-ranges-unavailable";
305
+ } else {
306
+ if (sourceByteIntervals === NO_SOURCE_BYTES) sourceByteIntervals = new Map();
307
+ sourceByteIntervals.set(sourceId, ranges);
298
308
  }
299
- unknownReason ??= "source-input-ranges-unavailable";
300
309
  }
301
310
  }
302
311
  }
@@ -456,10 +465,14 @@ export function predictPlaybackReadiness(input = {}) {
456
465
  const prepared = state.clientRanges.map((range) => ({ ...range }));
457
466
  const lastRange = state.segments.at(-1).ranges.at(-1);
458
467
  const trackEnd = lastRange ? earliest(end, lastRange.end) : end;
468
+ // What the page holds without a gap from the viewer only changes when a
469
+ // piece is added to it, so the end after one piece is the end before the
470
+ // next.
471
+ let after = contiguousEnd(delivered, at);
459
472
  for (const completion of completions.filter((item) => item.trackIndex === trackIndex)) {
460
- const before = contiguousEnd(delivered, at);
473
+ const before = after;
461
474
  for (const range of completion.segment.ranges) addMediaRange(delivered, range);
462
- const after = contiguousEnd(delivered, at);
475
+ after = contiguousEnd(delivered, at);
463
476
  const ahead = secondsAhead(earliest(before, trackEnd), at);
464
477
  constrain(completion.at - ahead, "delivery", trackIndex, completion.segment.index, completion.at, ahead, false);
465
478
  if (compare(after, before) <= 0 && completion.segment.ranges.some(({ start }) => compare(start, before) > 0)) {
@@ -468,7 +481,7 @@ export function predictPlaybackReadiness(input = {}) {
468
481
  return result(false, null, buffered, reserve, null, 'media-continuity-unavailable', preparedSegments);
469
482
  }
470
483
  }
471
- if (compare(contiguousEnd(delivered, at), trackEnd) < 0) {
484
+ if (compare(after, trackEnd) < 0) {
472
485
  // Everything this track still needs has been delivered in the schedule
473
486
  // and its media still stops short of the end: a hole between ranges
474
487
  // already held, which no arrival crosses.
@@ -807,6 +820,41 @@ function latestArrival(interval, ranges = []) {
807
820
  return finish;
808
821
  }
809
822
 
823
+ /** A piece that reads no source byte still to arrive; never written to. */
824
+ const NO_SOURCE_BYTES = new Map();
825
+
826
+ /**
827
+ * Whether the source named at `place` of a piece's sources — the track's own,
828
+ * then those its inputs name — was named at an earlier place.
829
+ */
830
+ function namedBefore(sourceId, place, trackSources, exactInputs) {
831
+ for (let earlier = 0; earlier < place; earlier += 1) {
832
+ const named = earlier < trackSources.length ? trackSources[earlier]
833
+ : exactInputs[earlier - trackSources.length]?.sourceId;
834
+ if (named === sourceId) return true;
835
+ }
836
+ return false;
837
+ }
838
+
839
+ /**
840
+ * The byte ranges a piece reads of one source, in the order its inputs state
841
+ * them, or null when an input of that source states none or one that is not a
842
+ * non-empty run of whole bytes.
843
+ */
844
+ function inputRangesOf(exactInputs, sourceId) {
845
+ let ranges = null;
846
+ for (const input of exactInputs) {
847
+ if (input?.sourceId !== sourceId) continue;
848
+ if (!Array.isArray(input.ranges) || input.ranges.length === 0) return null;
849
+ for (const range of input.ranges) {
850
+ if (!range || !Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) ||
851
+ range.start < 0 || range.end <= range.start) return null;
852
+ }
853
+ ranges = ranges === null ? input.ranges : ranges.concat(input.ranges);
854
+ }
855
+ return ranges;
856
+ }
857
+
810
858
  /** Exact gaps in sorted disjoint numeric coverage, without per-range splitting. */
811
859
  function uncoveredIntervals(interval, served) {
812
860
  let low = 0, high = served.length;