@torrent-tv/proxy 2.101.26 → 2.101.28

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.101.26",
3
+ "version": "2.101.28",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -281,6 +281,8 @@ export class LocalObservations {
281
281
  downloadSpeed: nonNegative(features.downloadSpeed),
282
282
  linkMbps: nonNegative(features.linkMbps),
283
283
  linkSampleAgeMs: nonNegative(features.linkSampleAgeMs),
284
+ preparedOutputBytes: nonNegative(features.preparedOutputBytes),
285
+ fileAvailable: typeof features.fileAvailable === "boolean" ? features.fileAvailable : null,
284
286
  headerRemaining: nonNegative(features.headerRemaining),
285
287
  peers: nonNegative(features.peers),
286
288
  at: this.#now()
@@ -291,9 +293,10 @@ export class LocalObservations {
291
293
 
292
294
  /**
293
295
  * Predict this host's first-frame wait from the closest measured start with
294
- * the same media suffix. Distance is the sum of squared log-ratios for the
295
- * independent measured inputs; it does not mix bytes, bit rates and peers
296
- * in their raw units.
296
+ * the same media suffix. Distance uses squared log-ratios for numeric
297
+ * inputs and exact-state distance for categorical inputs; it does not mix
298
+ * bytes, bit rates and peers in their raw units. Missing data contributes
299
+ * distance only when the other observation measured that input.
297
300
  *
298
301
  * @param {object} features
299
302
  * @returns {number | null}
@@ -303,24 +306,40 @@ export class LocalObservations {
303
306
  const readings = this.#entries.firstFrame[this.#firstFrameKey()] ?? [];
304
307
  const candidates = readings.filter(reading => reading.extension === (features.extension ?? ""));
305
308
  if (candidates.length === 0) return null;
306
- const dimensions = ["fileLength", "downloadSpeed", "linkMbps", "linkSampleAgeMs", "headerRemaining", "peers"];
309
+ const dimensions = ["fileLength", "downloadSpeed", "linkMbps", "linkSampleAgeMs", "preparedOutputBytes",
310
+ "fileAvailable", "headerRemaining", "peers"];
307
311
  const scales = new Map(dimensions.map(key => [key, median(candidates
308
- .map(reading => nonNegative(reading[key]))
312
+ .map(reading => key === "fileAvailable" ? null : nonNegative(reading[key]))
309
313
  .filter(value => value !== null && value > 0))]));
310
314
  let best = null;
311
315
  for (const reading of candidates) {
312
316
  let distance = 0;
313
317
  let used = 0;
318
+ let shared = 0;
314
319
  for (const key of dimensions) {
320
+ const current = key === "fileAvailable"
321
+ ? (typeof features[key] === "boolean" ? features[key] : null)
322
+ : nonNegative(features[key]);
323
+ const previous = key === "fileAvailable"
324
+ ? (typeof reading[key] === "boolean" ? reading[key] : null)
325
+ : nonNegative(reading[key]);
326
+ if (current === null && previous === null) continue;
327
+ if (current === null || previous === null) {
328
+ distance += 1;
329
+ used += 1;
330
+ continue;
331
+ }
332
+ shared += 1;
315
333
  const scale = scales.get(key);
316
- const current = nonNegative(features[key]);
317
- const previous = nonNegative(reading[key]);
318
- if (scale === null || current === null || previous === null) continue;
319
- const difference = Math.log1p(current / scale) - Math.log1p(previous / scale);
334
+ const difference = typeof current === "boolean" && typeof previous === "boolean"
335
+ ? Number(current !== previous)
336
+ : scale === null
337
+ ? Number(current !== previous)
338
+ : Math.log1p(current / scale) - Math.log1p(previous / scale);
320
339
  distance += difference * difference;
321
340
  used += 1;
322
341
  }
323
- if (used === 0) continue;
342
+ if (used === 0 || shared === 0) continue;
324
343
  distance /= used;
325
344
  if (!best || distance < best.distance || (distance === best.distance && reading.at > best.reading.at)) {
326
345
  best = { distance, reading };
@@ -119,6 +119,8 @@ export class ViewerRequests {
119
119
  linkMbps: Number.isFinite(linkMbps) && linkMbps > 0
120
120
  ? linkMbps : this.#host.viewers.get(consumerId)?.linkReading()?.linkMbps,
121
121
  linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null,
122
+ preparedOutputBytes: this.#preparedOutputBytes(sourceKey, fileIndex),
123
+ fileAvailable: null,
122
124
  headerRemaining: null,
123
125
  peers: null
124
126
  };
@@ -160,6 +162,9 @@ export class ViewerRequests {
160
162
  linkMbps: Number.isFinite(linkMbps) && linkMbps > 0
161
163
  ? linkMbps : this.#host.viewers.get(consumerId)?.linkReading()?.linkMbps,
162
164
  linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null,
165
+ preparedOutputBytes: Number.isSafeInteger(fileIndex) && fileIndex >= 0
166
+ ? this.#preparedOutputBytes(sourceKey, fileIndex) : null,
167
+ fileAvailable: typeof stats?.fileAvailable === "boolean" ? stats.fileAvailable : null,
163
168
  headerRemaining: Number.isFinite(stats?.headerBytes) && Number.isFinite(stats?.headerDownloadedBytes)
164
169
  ? Math.max(0, stats.headerBytes - stats.headerDownloadedBytes) : null,
165
170
  peers: stats?.connectedPeers ?? stats?.numPeers
@@ -221,6 +226,23 @@ export class ViewerRequests {
221
226
  return true;
222
227
  }
223
228
 
229
+ #preparedOutputBytes(sourceKey, fileIndex) {
230
+ const outputs = this.#host.outputs;
231
+ const segmentStore = this.#host.segmentStore;
232
+ if (!outputs?.values || !segmentStore?.sizesOf) return null;
233
+ let bytes = 0;
234
+ const seen = new Set();
235
+ for (const output of outputs.values()) {
236
+ if (output.file?.sourceKey !== sourceKey || output.file?.fileIndex !== fileIndex || !output.outputKey) continue;
237
+ if (seen.has(output.outputKey)) continue;
238
+ seen.add(output.outputKey);
239
+ for (const size of segmentStore.sizesOf(output.outputKey).values()) {
240
+ if (Number.isFinite(size) && size > 0) bytes += size;
241
+ }
242
+ }
243
+ return bytes;
244
+ }
245
+
224
246
  #pruneEarlyWaits() {
225
247
  const cutoff = Date.now() - 30 * 60_000;
226
248
  for (const [key, value] of this.#earlyWaits) {