@torrent-tv/proxy 2.95.32 → 2.95.34

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.32",
3
+ "version": "2.95.34",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -612,12 +612,13 @@ export class ViewerRequests {
612
612
  })).filter(range => range.end > range.start) } : sharedForecast;
613
613
  sourceMeasurements.set(sourceId, {
614
614
  id: sourceId,
615
+ serviceKey: output.file.sourceKey,
615
616
  complete: stats?.fileAvailable === true,
616
617
  residence: stats?.residence,
617
618
  fileOffset: stats?.fileOffset,
618
619
  fileLength: stats?.fileLength,
619
620
  pieceLength: stats?.pieceLength,
620
- downloadRateReadings: this.#sourceRateReadings(measurement, sourceId, stats, now),
621
+ downloadRateReadings: this.#sourceRateReadings(measurement, output.file.sourceKey, stats, now),
621
622
  downloadForecast,
622
623
  });
623
624
  }
@@ -37,6 +37,10 @@ import {
37
37
  ZERO
38
38
  } from "./media-time.js";
39
39
 
40
+ // Match the media element's allowance for a range that starts a few frames
41
+ // after its current position (see server/public/domain/buffer-metrics.js).
42
+ const CLIENT_RANGE_START_TOLERANCE_SECONDS = 0.25;
43
+
40
44
  /**
41
45
  * The mean rate predicted over a horizon by the integral of the measured rate.
42
46
  * Polling does not change the last measured service rate.
@@ -99,7 +103,8 @@ export class RateTrend {
99
103
  * @param {number} input.lookaheadSeconds
100
104
  * @param {number} input.now
101
105
  * @param {boolean} [input.requiredAudio]
102
- * @param {Array<{ id: string, complete: boolean,
106
+ * @param {Array<{ id: string, serviceKey?: string, complete: boolean, fileOffset?: number,
107
+ * fileLength?: number, pieceLength?: number, downloadRateReadings?: object,
103
108
  * downloadForecast?: { ranges: Array<{ start: number, end: number, availableAt: number | null }> } }>} input.sources
104
109
  * @param {Array<{ id: string, sourceIds: string[], processedSeconds: number,
105
110
  * bitsPerMediaSecond: number, readings: Array<{ at: number, value: number }>,
@@ -147,7 +152,7 @@ export function predictPlaybackReadiness(input = {}) {
147
152
  const position = toSeconds(at);
148
153
  const remaining = Math.max(0, duration - position);
149
154
  const measuredRanges = reported.map((ranges, trackIndex) => ranges ?
150
- heldRanges(ranges, segmentsOf[trackIndex], unit) : null);
155
+ heldRanges(ranges, segmentsOf[trackIndex], unit, at) : null);
151
156
  const measured = measuredRanges.length > 0 && measuredRanges.every(Boolean);
152
157
  const heldEnd = measured ? earliest(...measuredRanges.map((ranges) => contiguousEnd(ranges, at))) : null;
153
158
  // The figure reported back is what the page said it holds; the widening by
@@ -192,8 +197,16 @@ export function predictPlaybackReadiness(input = {}) {
192
197
  return result(false, null, buffered, reserve, null, "timeline-unavailable", 0);
193
198
  }
194
199
  const sourceState = new Map();
200
+ // A torrent has one download service, even when required media lives in separate files.
201
+ const sourceServices = new Map();
195
202
  for (const [id, source] of sources) {
196
- sourceState.set(id, { source, rate: rateCurve(source.downloadRateReadings, now), serviceFinish: 0 });
203
+ const serviceKey = typeof source.serviceKey === "string" ? source.serviceKey : id;
204
+ let service = sourceServices.get(serviceKey);
205
+ if (!service) {
206
+ service = { rate: rateCurve(source.downloadRateReadings, now), finish: 0 };
207
+ sourceServices.set(serviceKey, service);
208
+ }
209
+ sourceState.set(id, { source, service });
197
210
  }
198
211
  const link = rateCurve(input.linkReadings, now);
199
212
  let unknownReason = null;
@@ -333,7 +346,8 @@ export function predictPlaybackReadiness(input = {}) {
333
346
  // pieces, project only the media-order demand from the torrent's latest
334
347
  // measured download service; no supplier or positive rate means no
335
348
  // estimated arrival.
336
- const { source: facts, rate } = source;
349
+ const { source: facts, service: sourceService } = source;
350
+ const { rate } = sourceService;
337
351
  if (!rate || !(rate.rateAt(0) > 0) || !Number.isSafeInteger(facts.pieceLength) ||
338
352
  facts.pieceLength <= 0 || !Number.isSafeInteger(facts.fileOffset) ||
339
353
  !Number.isSafeInteger(facts.fileLength) || facts.fileLength <= 0) {
@@ -342,7 +356,7 @@ export function predictPlaybackReadiness(input = {}) {
342
356
  unavailableSource: { sourceId, segmentIndex: segment.index, range: interval, missing } };
343
357
  }
344
358
  const estimatedPieces = sourcePiecesFor(missing, facts);
345
- let finish = Math.max(source.serviceFinish, sourceArrivalAt);
359
+ let finish = Math.max(sourceService.finish, sourceArrivalAt);
346
360
  const service = sourceByteService.get(sourceId) ?? [];
347
361
  for (const pieceRange of estimatedPieces) {
348
362
  const stillMissing = uncoveredIntervals(pieceRange, sourceByteCoverage.get(sourceId) ?? []);
@@ -354,7 +368,7 @@ export function predictPlaybackReadiness(input = {}) {
354
368
  sourceByteCoverage.set(sourceId, numericCoverage(service));
355
369
  sourceArrivals.set(sourceId, indexedArrivals(service));
356
370
  }
357
- source.serviceFinish = finish;
371
+ sourceService.finish = finish;
358
372
  }
359
373
  }
360
374
  // Production accepts a complete held segment input. It cannot overlap its
@@ -501,13 +515,18 @@ function joinedPiece(served) {
501
515
  * @param {bigint} unit
502
516
  * @returns {import("./media-time.js").MediaRange[]}
503
517
  */
504
- function heldRanges(ranges, segments, unit) {
518
+ function heldRanges(ranges, segments, unit, position) {
505
519
  const pieces = segments.flatMap((segment) => segment.ranges ?? []);
506
520
  const zero = mediaTime(0n, unit);
507
521
  const error = rescale(REPORTED_TIME_ERROR, unit);
522
+ const startTolerance = add(position, rescale(fromSeconds(CLIENT_RANGE_START_TOLERANCE_SECONDS), unit));
508
523
  const held = [];
509
524
  for (const range of ranges) {
510
- const start = latest(zero, subtract(rescale(range.start, unit), error));
525
+ const reportedStart = rescale(range.start, unit);
526
+ let start = latest(zero, subtract(reportedStart, error));
527
+ if (compare(reportedStart, position) > 0 && compare(reportedStart, startTolerance) <= 0) {
528
+ start = position;
529
+ }
511
530
  const end = add(rescale(range.end, unit), error);
512
531
  const inside = pieces.filter((piece) => contains([{ start, end }], piece.start, piece.end));
513
532
  addMediaRange(held, { start, end, frame: latest(zero, ...inside.map(({ frame }) => frame)) });