@torrent-tv/proxy 2.87.7 → 2.87.9

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 2.87.9
2
+
3
+ - **Fix**: Stop refreshing late sidecar metadata when the completed header read reports no audio tracks.
4
+
5
+ ## 2.87.8
6
+
7
+ - **Fix**: Keep a viewer registered until every data channel on its WebRTC connection closes, and ignore a closing connection after the viewer has identified themselves on a replacement connection.
8
+
1
9
  ## 2.87.7
2
10
 
3
11
  - **Fix**: Refresh audio metadata for sidecar tracks after a delayed container-header read, keeping the existing track numbers and rendition count stable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.87.7",
3
+ "version": "2.87.9",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -445,7 +445,7 @@ export function createPlaybackPlanner({
445
445
  tracks: tracks
446
446
  .filter((track) => track?.type === "audio")
447
447
  .sort((left, right) => (left.declaredIndex ?? 0) - (right.declaredIndex ?? 0)),
448
- complete: Array.isArray(tracks) && tracks.length > 0
448
+ complete: Array.isArray(tracks)
449
449
  }))
450
450
  .catch((error) => {
451
451
  logger.info(`audio tracks: "${label}" could not be read (${error?.message ?? error})`);
@@ -312,7 +312,7 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
312
312
  *
313
313
  * @returns {void}
314
314
  */
315
- const stop = () => {
315
+ const stop = (because = "the transport stopped answering") => {
316
316
  if (stopped) {
317
317
  return;
318
318
  }
@@ -332,7 +332,7 @@ function makeSendQueueWatcher({ log, getTransportSnapshot, witness, usrsctpState
332
332
  // because the first does not always come: a peer connection can die
333
333
  // without `onClosed`, which is why this watch was written in the first
334
334
  // place.
335
- onConnectionGone?.(sessionId, "the transport stopped answering");
335
+ onConnectionGone?.(sessionId, because);
336
336
  }
337
337
  };
338
338
  // Independent of the queue: the transport's own counters, sampled for as
@@ -697,6 +697,16 @@ export function createDataChannelHandler({
697
697
  * @type {Map<string, Set<string>>}
698
698
  */
699
699
  const viewersOnConnection = new Map();
700
+ /**
701
+ * The newest connection currently carrying each viewer's identity.
702
+ *
703
+ * A reconnect can open its channels before the old connection finishes
704
+ * closing. The old connection must not release a viewer who has already
705
+ * identified themselves on the replacement.
706
+ *
707
+ * @type {Map<string, string>}
708
+ */
709
+ const connectionOfViewer = new Map();
700
710
 
701
711
  /**
702
712
  * Everyone on this connection has gone, because the connection has.
@@ -716,6 +726,10 @@ export function createDataChannelHandler({
716
726
  return;
717
727
  }
718
728
  for (const consumerId of viewers) {
729
+ if (connectionOfViewer.get(consumerId) !== sessionId) {
730
+ continue;
731
+ }
732
+ connectionOfViewer.delete(consumerId);
719
733
  onLog?.(`[dc] Session ${sessionId.slice(0, 8)}: viewer ${consumerId} left — ${because}`);
720
734
  try {
721
735
  onViewerGone(consumerId, because);
@@ -997,6 +1011,7 @@ export function createDataChannelHandler({
997
1011
  const isNew = !known.has(message.consumerId);
998
1012
  known.add(message.consumerId);
999
1013
  viewersOnConnection.set(sessionId, known);
1014
+ connectionOfViewer.set(message.consumerId, sessionId);
1000
1015
  channelOfViewer.set(message.consumerId, channel);
1001
1016
  if (isNew) {
1002
1017
  log(`[dc] Session ${tag}: viewer ${message.consumerId} is on this connection`);
@@ -1030,7 +1045,7 @@ export function createDataChannelHandler({
1030
1045
  });
1031
1046
 
1032
1047
  channel.onClosed(() => {
1033
- stopWatchdog();
1048
+ stopWatchdog("the connection closed");
1034
1049
  deliveryProbe.detach(sessionId, channel);
1035
1050
  for (const entry of partials.values()) {
1036
1051
  clearTimeout(entry.timer);
@@ -1038,11 +1053,9 @@ export function createDataChannelHandler({
1038
1053
  partials.clear();
1039
1054
  forgetChannel(channel);
1040
1055
  log(`[dc] Session ${tag}: channel closed`);
1041
- // The ordinary way a viewer leaves, and the only one that is immediate.
1042
- // Everything else — a silence long enough to be called an absence, the
1043
- // transport's own counters going quiet — is a backstop for this event
1044
- // failing to arrive.
1045
- connectionGone(sessionId, "the connection closed");
1056
+ // One closed data channel does not mean the peer connection is gone:
1057
+ // media, control and probe channels can close independently. The shared
1058
+ // transport watcher calls `connectionGone` when its last channel closes.
1046
1059
  });
1047
1060
 
1048
1061
  channel.onError((err) => {
@@ -0,0 +1,92 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createDataChannelHandler } from "../services/transport/data-channel-handler.js";
4
+
5
+ class FakeDataChannel {
6
+ constructor(label) {
7
+ this.label = label;
8
+ this.messages = [];
9
+ this.closed = null;
10
+ }
11
+
12
+ getLabel() {
13
+ return this.label;
14
+ }
15
+
16
+ onMessage(callback) {
17
+ this.message = callback;
18
+ }
19
+
20
+ onClosed(callback) {
21
+ this.closed = callback;
22
+ }
23
+
24
+ onError() {}
25
+
26
+ sendMessage(message) {
27
+ this.messages.push(message);
28
+ }
29
+
30
+ close() {
31
+ this.closed?.();
32
+ }
33
+ }
34
+
35
+ test("a viewer remains present until every data channel on the connection closes", () => {
36
+ const gone = [];
37
+ const handler = createDataChannelHandler({
38
+ proxyPort: 9090,
39
+ getTransportSnapshot: () => ({ bytesSent: 0, bytesReceived: 0 }),
40
+ onViewerGone: (consumerId, reason) => gone.push({ consumerId, reason })
41
+ });
42
+ const media = new FakeDataChannel("proxy");
43
+ const control = new FakeDataChannel("proxy-control");
44
+ const probe = new FakeDataChannel("proxy-fast");
45
+
46
+ handler.handleChannel("peer-session", media);
47
+ handler.handleChannel("peer-session", control);
48
+ handler.handleChannel("peer-session", probe);
49
+ control.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
50
+
51
+ control.close();
52
+ assert.deepEqual(gone, [], "closing the control channel alone does not release the viewer");
53
+
54
+ media.close();
55
+ assert.deepEqual(gone, [], "the viewer remains while the probe channel is still open");
56
+
57
+ probe.close();
58
+ assert.deepEqual(gone, [
59
+ { consumerId: "viewer-1", reason: "the connection closed" }
60
+ ]);
61
+ });
62
+
63
+ test("a closing connection does not release a viewer already named on its replacement", () => {
64
+ const gone = [];
65
+ const handler = createDataChannelHandler({
66
+ proxyPort: 9090,
67
+ getTransportSnapshot: () => ({ bytesSent: 0, bytesReceived: 0 }),
68
+ onViewerGone: (consumerId, reason) => gone.push({ consumerId, reason })
69
+ });
70
+ const oldControl = new FakeDataChannel("proxy-control");
71
+ const oldMedia = new FakeDataChannel("proxy");
72
+ const newControl = new FakeDataChannel("proxy-control");
73
+ const newMedia = new FakeDataChannel("proxy");
74
+
75
+ handler.handleChannel("old-peer", oldControl);
76
+ handler.handleChannel("old-peer", oldMedia);
77
+ oldControl.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
78
+ handler.handleChannel("new-peer", newControl);
79
+ handler.handleChannel("new-peer", newMedia);
80
+ newControl.message(JSON.stringify({ type: "viewer", consumerId: "viewer-1" }));
81
+
82
+ oldControl.close();
83
+ oldMedia.close();
84
+ assert.deepEqual(gone, [], "the old connection no longer owns the viewer");
85
+
86
+ newControl.close();
87
+ assert.deepEqual(gone, [], "the replacement still has its media channel");
88
+ newMedia.close();
89
+ assert.deepEqual(gone, [
90
+ { consumerId: "viewer-1", reason: "the connection closed" }
91
+ ]);
92
+ });