@torrent-tv/proxy 2.107.13 → 2.107.15

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.107.13",
3
+ "version": "2.107.15",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -1383,6 +1383,9 @@ export async function startProxyServer({
1383
1383
  const held = await torrentPool.heldRangesOf(torrent, work.fileIndex);
1384
1384
  return work.inputRanges.every(([start, end]) => held.some(([from, to]) => from <= start && to >= end));
1385
1385
  },
1386
+ sourceRestNeededFor: (sourceKey, fileIndex) => ![...outputParts.outputs.values()].some(output =>
1387
+ output.file?.sourceKey === sourceKey && output.file?.fileIndex === fileIndex &&
1388
+ outputParts.viewers.forOutput(output).size > 0),
1386
1389
  playbackAppliedFor: (sourceKey, fileIndex) => downloadMaps.hasAppliedPlayback(sourceKey, fileIndex),
1387
1390
  readyChanged: (work, ready) => logger.info(`source urgent request=${work.requestId} viewer=${work.ownerId} ` +
1388
1391
  `source=${work.sourceKey} file=${work.fileIndex} inputReady=${ready}`),
@@ -41,6 +41,34 @@ function sameInputs(left, right) {
41
41
  return left.every((input, index) => input.sourceId === right[index].sourceId && input.ranges === right[index].ranges);
42
42
  }
43
43
 
44
+ function forecastInputState(tracks, identity) {
45
+ if (!identity) return null;
46
+ const track = tracks.find(candidate => candidate.id === identity.trackId);
47
+ const segment = track?.segments.find(candidate => candidate.index === identity.segmentIndex);
48
+ const sourceInput = segment?.sourceInputs?.find(candidate => candidate.sourceId === identity.sourceId);
49
+ return {
50
+ sourceId: identity.sourceId,
51
+ trackId: identity.trackId,
52
+ segmentIndex: identity.segmentIndex,
53
+ phase: identity.phase,
54
+ rangesKnown: Array.isArray(sourceInput?.ranges),
55
+ rangeCount: Array.isArray(sourceInput?.ranges) ? sourceInput.ranges.length : null
56
+ };
57
+ }
58
+
59
+ function forecastInputIdentity(state) {
60
+ return state && JSON.stringify([state.sourceId, state.trackId, state.segmentIndex, state.phase]);
61
+ }
62
+
63
+ function observeForecastInput(observeWait, sourceKey, state, event) {
64
+ if (!state) return;
65
+ observeWait?.(sourceKey, "process", {
66
+ process: "P4.2", phase: event, at: performance.now(),
67
+ sourceId: state.sourceId, outputKey: state.trackId, segment: state.segmentIndex,
68
+ forecastPhase: state.phase, rangesKnown: state.rangesKnown, rangeCount: state.rangeCount
69
+ });
70
+ }
71
+
44
72
  function segmentAddressOf(output, fileName) {
45
73
  const format = output.segmentFormat;
46
74
  if (!format) {
@@ -1090,6 +1118,16 @@ export class ViewerRequests {
1090
1118
  if (waitEstimate) forecast.waitEstimate = waitEstimate;
1091
1119
  }
1092
1120
  const unavailableInput = forecast.unavailableSource;
1121
+ const previousInputState = measurement.forecastInputState ?? null;
1122
+ const currentInputState = forecastInputState(tracks, unavailableInput);
1123
+ if (previousInputState && forecastInputIdentity(previousInputState) !== forecastInputIdentity(currentInputState)) {
1124
+ observeForecastInput(this.#host.observeWait, session.file.sourceKey, previousInputState,
1125
+ currentInputState ? "forecast-blocker-replaced" : "forecast-blocker-cleared");
1126
+ }
1127
+ if (currentInputState && JSON.stringify(previousInputState) !== JSON.stringify(currentInputState)) {
1128
+ observeForecastInput(this.#host.observeWait, session.file.sourceKey, currentInputState, "forecast-blocked");
1129
+ }
1130
+ measurement.forecastInputState = currentInputState;
1093
1131
  const forecastInputKey = unavailableInput ? JSON.stringify([
1094
1132
  unavailableInput.sourceId, unavailableInput.trackId, unavailableInput.segmentIndex, unavailableInput.phase
1095
1133
  ]) : null;
@@ -37,6 +37,7 @@ export class SourcePreparation {
37
37
  #failed;
38
38
  #reprice;
39
39
  #inputReady;
40
+ #sourceRestNeededFor;
40
41
  #readyChanged;
41
42
  #playbackAppliedFor;
42
43
  #work = new Map();
@@ -47,7 +48,7 @@ export class SourcePreparation {
47
48
 
48
49
  constructor({ viewers, candidatesFor, relatedFilesFor = () => [], priorityFor = (_viewer, priority) => priority,
49
50
  inputReady = async () => false, readyChanged = () => {}, playbackAppliedFor = () => true,
50
- inspect, withdraw, reprice = () => {}, failed }) {
51
+ sourceRestNeededFor = () => true, inspect, withdraw, reprice = () => {}, failed }) {
51
52
  this.#viewers = viewers;
52
53
  this.#candidates = candidatesFor;
53
54
  this.#related = relatedFilesFor;
@@ -57,6 +58,7 @@ export class SourcePreparation {
57
58
  this.#failed = failed;
58
59
  this.#reprice = reprice;
59
60
  this.#inputReady = inputReady;
61
+ this.#sourceRestNeededFor = sourceRestNeededFor;
60
62
  this.#readyChanged = readyChanged;
61
63
  this.#playbackAppliedFor = playbackAppliedFor;
62
64
  }
@@ -83,6 +85,7 @@ export class SourcePreparation {
83
85
 
84
86
  /** Current owners determine priority; old work never restores a withdrawn read. */
85
87
  demandFor(work) {
88
+ if (work.role === "source-rest" && !this.#sourceRestNeededFor(work.sourceKey, work.fileIndex)) return null;
86
89
  const handoffFileIndex = work.selected || (work.role === "source-rest" && work.ownerFileIndex === work.fileIndex)
87
90
  ? work.fileIndex : null;
88
91
  const opening = Number.isInteger(handoffFileIndex) && this.#viewers.forSource(work.sourceKey).some(viewer =>