@torrent-tv/proxy 2.99.4 → 2.99.6

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.4",
3
+ "version": "2.99.6",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -653,10 +653,16 @@ export class MatroskaContainer extends Container {
653
653
  * One preceding cluster retains preroll; one following cluster covers demux
654
654
  * lookahead beyond the presentation cut. No packet payload is parsed here.
655
655
  */
656
- async supportsOriginalSourceRanges() {
656
+ async supportsOriginalSourceRanges(interval = null) {
657
657
  const cues = await this.readCues();
658
658
  if (!cues?.points.length) return false;
659
659
  const video = ContainerTrack.firstUsable(await this.readTracks(), "video");
660
+ // Sound alone is read from its own packets, as an AVI's is
661
+ // (`AviContainer`). From the original file it was given whole clusters,
662
+ // picture included: 260 MB for 23 pieces of a soundtrack, 38 MB for one of
663
+ // 171 KB, on Home Assistant (r44, r46, torrent-tv/meta#178). Asked of the
664
+ // file as a whole, the answer is the picture's.
665
+ if (video && interval?.trackIds?.length && !interval.trackIds.includes(video.trackNumber)) return false;
660
666
  return cues.points.some(point => point.positions.some(position => !video || position.track === video.trackNumber));
661
667
  }
662
668
 
@@ -1248,7 +1248,9 @@ export function createDataChannelHandler({
1248
1248
  const loop = eventLoopDelay();
1249
1249
  const mbps = elapsedMs > 0 ? (totalBytes * 8) / (elapsedMs * 1000) : 0;
1250
1250
  log(
1251
- `[net-debug] sent ${path}${queryInfo} bytes=${totalBytes} fetchMs=${fetchMs} ` +
1251
+ // The request's own id, so the page's line for the same answer can
1252
+ // be read beside this one without comparing two machines' clocks.
1253
+ `[net-debug] sent ${path}${queryInfo} requestId=${requestId} bytes=${totalBytes} fetchMs=${fetchMs} ` +
1252
1254
  `ttfbMs=${firstByteMs} sendMs=${elapsedMs} chunks=${chunks} ` +
1253
1255
  // The size of one message, so a reading can be attributed to the
1254
1256
  // size it was taken at rather than to the run it came from.
@@ -1335,54 +1337,86 @@ export function createDataChannelHandler({
1335
1337
  }
1336
1338
 
1337
1339
  /**
1338
- * Resolve once the channel's outgoing buffer has drained below the low-water
1339
- * mark. No-op (resolves immediately) when the buffer is already small or the
1340
- * channel does not expose buffer APIs. The previous implementation resolved
1341
- * after {@link DC_BUFFER_DRAIN_TIMEOUT_MS} even though the buffer was still
1342
- * high, so the send loop kept queueing into a wedged channel and the queue
1343
- * grew to 399 MB (field 2026-09-06). This version waits until the buffer
1344
- * actually drains, polling as a fallback for a missed low-water event, and
1345
- * never resolves while the channel is still holding bytes above the low-water
1346
- * mark.
1340
+ * The send loops waiting on each channel for its library queue to empty. The
1341
+ * native channel has one low-water callback, so it is set once per channel
1342
+ * and wakes every loop waiting on it; each then reads the queue itself.
1347
1343
  *
1344
+ * @type {WeakMap<DataChannel, Set<() => void>>}
1345
+ */
1346
+ const drainWaiters = new WeakMap();
1347
+
1348
+ /**
1348
1349
  * @param {DataChannel} channel
1350
+ * @returns {Set<() => void> | null} Null for a channel that cannot say when its queue empties.
1351
+ */
1352
+ function drainWaitersOf(channel) {
1353
+ let waiters = drainWaiters.get(channel);
1354
+ if (waiters) return waiters;
1355
+ if (typeof channel.onBufferedAmountLow !== "function" || typeof channel.setBufferedAmountLowThreshold !== "function") {
1356
+ return null;
1357
+ }
1358
+ waiters = new Set();
1359
+ drainWaiters.set(channel, waiters);
1360
+ channel.setBufferedAmountLowThreshold(0);
1361
+ channel.onBufferedAmountLow(() => {
1362
+ for (const wake of [...waiters]) wake();
1363
+ });
1364
+ return waiters;
1365
+ }
1366
+
1367
+ /**
1368
+ * Resolve once the library holds nothing of this channel's that usrsctp has
1369
+ * not yet taken, or the request is cancelled.
1370
+ *
1371
+ * WHY EMPTY, AND NOT A HIGH-WATER MARK. libdatachannel keeps ONE queue for
1372
+ * every channel of a connection, first in first out (`SctpTransport::send`,
1373
+ * v0.24.2), and usrsctp's own send buffer sits after it. A control answer —
1374
+ * the forecast a held viewer waits for, a seek's acknowledgement — waits
1375
+ * behind everything handed over before it, on every channel. With the 8 MiB
1376
+ * mark this was, and with usrsctp's buffer sized by the 16 MiB message limit,
1377
+ * probes on all three channels stood 2.8-5.1 s behind on Home Assistant while
1378
+ * copied 1080p pieces of 11-12 MB crossed, and a viewer whose picture was
1379
+ * already buffered was started that much later (r46, torrent-tv/meta#178).
1380
+ * usrsctp's own buffer keeps the wire busy while this loop is away; whether
1381
+ * it is deep enough for the rate and this thread's pauses is read from the
1382
+ * `sent` line's rate and event-loop figures, not assumed.
1383
+ *
1384
+ * Cancellation ends the wait at once; elapsed time alone never resumes
1385
+ * sending, so a wedged channel does not grow a queue (399 MB, field
1386
+ * 2026-09-06).
1387
+ *
1388
+ * @param {DataChannel} channel
1389
+ * @param {AbortSignal} signal
1349
1390
  * @returns {Promise<void>}
1350
1391
  */
1351
1392
  function waitForBufferDrain(channel, signal) {
1352
1393
  return new Promise((resolve) => {
1353
1394
  try {
1354
- if (signal.aborted || typeof channel.bufferedAmount !== "function" || channel.bufferedAmount() <= DC_BUFFER_HIGH_WATER) {
1395
+ if (signal.aborted || typeof channel.bufferedAmount !== "function" || channel.bufferedAmount() <= 0) {
1355
1396
  resolve();
1356
1397
  return;
1357
1398
  }
1358
- let poll = null;
1399
+ const waiters = drainWaitersOf(channel);
1359
1400
  let settled = false;
1360
1401
  const done = () => {
1361
1402
  if (settled) return;
1362
1403
  settled = true;
1363
- if (poll) clearInterval(poll);
1404
+ waiters?.delete(wake);
1364
1405
  signal.removeEventListener("abort", done);
1365
1406
  resolve();
1366
1407
  };
1367
- signal.addEventListener("abort", done, { once: true });
1368
- // The native channel exposes one callback for all requests. Sampling
1369
- // avoids one request replacing another's callback. Cancellation ends
1370
- // the wait immediately; elapsed time alone never resumes sending.
1371
- poll = setInterval(() => {
1408
+ const wake = () => {
1372
1409
  try {
1373
- if (channel.bufferedAmount() <= DC_BUFFER_LOW_WATER) {
1374
- done();
1375
- }
1410
+ if (channel.bufferedAmount() <= 0) done();
1376
1411
  } catch {
1377
1412
  done();
1378
1413
  }
1379
- }, 50);
1380
- if (typeof poll.unref === "function") poll.unref();
1381
- // Guard against a race where the buffer drained between the check above
1382
- // and registering the callback (the low-water event would never fire).
1383
- if (channel.bufferedAmount() <= DC_BUFFER_LOW_WATER) {
1384
- done();
1385
- }
1414
+ };
1415
+ signal.addEventListener("abort", done, { once: true });
1416
+ waiters?.add(wake);
1417
+ // The queue may have emptied between the first reading and the
1418
+ // registration, when no further low-water event would come.
1419
+ wake();
1386
1420
  if (signal.aborted) done();
1387
1421
  } catch {
1388
1422
  resolve();
@@ -1461,6 +1495,3 @@ const TRANSPORT_HEARTBEAT_MS = 5_000;
1461
1495
  // registry does not end a healthy watch.
1462
1496
  const TRANSPORT_UNKNOWN_HEARTBEATS = 3;
1463
1497
  const SEND_QUEUE_STUCK_MS = 5_000;
1464
- const DC_BUFFER_HIGH_WATER = 8 * 1024 * 1024;
1465
- /** Resume sending once the channel buffer drains to this many bytes. */
1466
- const DC_BUFFER_LOW_WATER = 1 * 1024 * 1024;
@@ -26,16 +26,20 @@ const ICE_SERVERS = ["stun:stun.l.google.com:19302", "stun:stun.cloudflare.com:3
26
26
  const PORT_PREDICTION_WINDOW = 16;
27
27
 
28
28
  // Max size of a single data-channel message the proxy advertises (SDP
29
- // `a=max-message-size`) and will accept. The browser caps `channel.send()` at
30
- // the REMOTE's advertised value, so this is what lets the browser send a large
31
- // request body in one message — notably registering a source, whose body is
32
- // the base64-encoded .torrent. A big multi-season pack's .torrent (thousands of
33
- // piece hashes) can be hundreds of KB (e.g. Poirot: 420 KB → ~560 KB base64),
34
- // which exceeds libdatachannel's ~256 KB default and made `send()` throw
35
- // "message larger than max-message-size". 16 MB is a generous ceiling (memory
36
- // is allocated per actual message, not reserved). Responses (proxy→browser)
37
- // are already safe — they stream in small reader-sized chunks.
38
- const MAX_DC_MESSAGE_BYTES = 16 * 1024 * 1024;
29
+ // `a=max-message-size`) and will accept: libdatachannel's own default.
30
+ //
31
+ // It was 16 MiB so that a page could send a large .torrent in one message
32
+ // (Poirot: ~560 KB base64). The page has cut every request body above 128 KiB
33
+ // into 64 KiB frames since server 0.8.58 (`REQUEST_CHUNK_BYTES`), and the
34
+ // proxy's responses go in reader-sized frames, so no message needs more.
35
+ //
36
+ // What 16 MiB also did: libdatachannel sizes usrsctp's send buffer to at least
37
+ // this value (`SctpTransport`, v0.24.2), and its queue is shared by every
38
+ // channel of a connection. A forecast answer or a probe then waited behind up
39
+ // to 16 MiB of video: 2.8-5.1 s on Home Assistant while copied 1080p pieces
40
+ // crossed (r46, torrent-tv/meta#178). At the default the buffer is the 1 MiB
41
+ // libdatachannel gives usrsctp (`SctpTransport::SetSettings`).
42
+ const MAX_DC_MESSAGE_BYTES = 256 * 1024;
39
43
 
40
44
  /**
41
45
  * Build predicted srflx ICE candidates for a symmetric NAT.