@torrent-tv/proxy 2.106.0 → 2.106.1

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.106.0",
3
+ "version": "2.106.1",
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
@@ -23,6 +23,7 @@ import { trackChoicesOf } from "./services/viewer/segment-demands.js";
23
23
  import { PlaybackProjection } from "./services/torrent/PlaybackProjection.js";
24
24
  import { SegmentInputTable } from "./services/viewer/segment-input-table.js";
25
25
  import { SourcePreparation, preparationInterval } from "./services/viewer/SourcePreparation.js";
26
+ import { publishPlaybackBeforePreparationRefresh } from "./services/server/publish-playback-before-preparation-refresh.js";
26
27
  import { handleHealthzGet } from "./routes/healthz/get.js";
27
28
  import { handleApiDeliverySinkGet } from "./routes/api/delivery-sink/get.js";
28
29
  import { handleApiLinkProbeGet } from "./routes/api/link-probe/get.js";
@@ -1213,7 +1214,6 @@ export async function startProxyServer({
1213
1214
  setDownloadRate: (bytesPerSecond, because) => torrentPool.setDownloadRate?.(bytesPerSecond, because),
1214
1215
  setSpeculativeHold: (held, because) => torrentPool.setSpeculativeHold?.(held, because),
1215
1216
  setPriorityMap: async ({ sourceKey, fileIndex, durationSeconds, zones }) => {
1216
- void sourcePreparation?.refresh();
1217
1217
  const record = sourceRegistry.get(sourceKey);
1218
1218
  if (!record) {
1219
1219
  return;
@@ -1224,9 +1224,13 @@ export async function startProxyServer({
1224
1224
  if (!outputParts.viewers.forSource(sourceKey).length) probeReads.forget(sourceKey, fileIndex);
1225
1225
  await downloadMaps.forget(sourceKey, fileIndex, { keepPreparation: true });
1226
1226
  playbackProjections.get(`${sourceKey}:${fileIndex}`)?.clear();
1227
+ void sourcePreparation?.refresh();
1227
1228
  return;
1228
1229
  }
1229
- await downloadMaps.playback({ sourceKey, fileIndex, durationSeconds, zones });
1230
+ await publishPlaybackBeforePreparationRefresh(
1231
+ () => downloadMaps.playback({ sourceKey, fileIndex, durationSeconds, zones }),
1232
+ () => { void sourcePreparation?.refresh(); }
1233
+ );
1230
1234
  mediaReads.retain(sourceKey, fileIndex, params => !params.requestId?.startsWith("download:") ||
1231
1235
  zones.some(zone => zone.from === params.downloadInterval?.from && zone.to === params.downloadInterval?.to));
1232
1236
  } catch (error) {
@@ -1375,6 +1379,7 @@ export async function startProxyServer({
1375
1379
  const held = await torrentPool.heldRangesOf(torrent, work.fileIndex);
1376
1380
  return work.inputRanges.every(([start, end]) => held.some(([from, to]) => from <= start && to >= end));
1377
1381
  },
1382
+ playbackAppliedFor: (sourceKey, fileIndex) => downloadMaps.hasAppliedPlayback(sourceKey, fileIndex),
1378
1383
  readyChanged: (work, ready) => logger.info(`source urgent request=${work.requestId} viewer=${work.ownerId} ` +
1379
1384
  `source=${work.sourceKey} file=${work.fileIndex} inputReady=${ready}`),
1380
1385
  inspect: async work => {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Keep source preparation stated until the viewer's playback map is applied.
3
+ *
4
+ * @param {() => Promise<unknown>} publishPlayback
5
+ * @param {() => unknown} refreshPreparation
6
+ * @returns {Promise<void>}
7
+ */
8
+ export async function publishPlaybackBeforePreparationRefresh(publishPlayback, refreshPreparation) {
9
+ await publishPlayback();
10
+ refreshPreparation();
11
+ }
@@ -162,6 +162,11 @@ export class DownloadMaps {
162
162
  return this.#files.get(`${sourceKey}:${fileIndex}`)?.epoch ?? 0;
163
163
  }
164
164
 
165
+ hasAppliedPlayback(sourceKey, fileIndex) {
166
+ const file = this.#files.get(`${sourceKey}:${fileIndex}`);
167
+ return Boolean(file?.zones.length && file.shape && file.appliedShape === file.shape);
168
+ }
169
+
165
170
  native({ sourceKey, fileIndex, zones }) {
166
171
  const file = this.#file(sourceKey, fileIndex);
167
172
  file.nativeZones = zones;
@@ -325,7 +330,12 @@ export class DownloadMaps {
325
330
  const answeredAt = this.#clock();
326
331
  // A torrent not yet in the thread applies nothing, so this map was not
327
332
  // delivered and the next one, however alike, has to be.
328
- if (reply?.appliedMs === null && file.shape === shape) file.shape = null;
333
+ if (reply?.appliedMs === null) {
334
+ if (file.shape === shape) file.shape = null;
335
+ if (file.appliedShape === shape) file.appliedShape = null;
336
+ } else {
337
+ file.appliedShape = shape;
338
+ }
329
339
  // The window open now, which a report since this map was asked for may have started.
330
340
  const stats = this.#statsOf(file);
331
341
  stats.sent += 1;
@@ -345,6 +355,7 @@ export class DownloadMaps {
345
355
  // sent, and leaves nothing to compare the next one against.
346
356
  file.pending = pending.catch(() => {
347
357
  if (file.shape === shape) file.shape = null;
358
+ if (file.appliedShape === shape) file.appliedShape = null;
348
359
  });
349
360
  return pending;
350
361
  }
@@ -38,6 +38,7 @@ export class SourcePreparation {
38
38
  #reprice;
39
39
  #inputReady;
40
40
  #readyChanged;
41
+ #playbackAppliedFor;
41
42
  #work = new Map();
42
43
  #revision = 0;
43
44
  #completedRevision = null;
@@ -45,7 +46,8 @@ export class SourcePreparation {
45
46
  #candidateAbort = new AbortController();
46
47
 
47
48
  constructor({ viewers, candidatesFor, relatedFilesFor = () => [], priorityFor = (_viewer, priority) => priority,
48
- inputReady = async () => false, readyChanged = () => {}, inspect, withdraw, reprice = () => {}, failed }) {
49
+ inputReady = async () => false, readyChanged = () => {}, playbackAppliedFor = () => true,
50
+ inspect, withdraw, reprice = () => {}, failed }) {
49
51
  this.#viewers = viewers;
50
52
  this.#candidates = candidatesFor;
51
53
  this.#related = relatedFilesFor;
@@ -56,6 +58,7 @@ export class SourcePreparation {
56
58
  this.#reprice = reprice;
57
59
  this.#inputReady = inputReady;
58
60
  this.#readyChanged = readyChanged;
61
+ this.#playbackAppliedFor = playbackAppliedFor;
59
62
  }
60
63
 
61
64
  refresh() {
@@ -80,8 +83,13 @@ export class SourcePreparation {
80
83
 
81
84
  /** Current owners determine priority; old work never restores a withdrawn read. */
82
85
  demandFor(work) {
86
+ const handoffFileIndex = work.selected || (work.role === "source-rest" && work.ownerFileIndex === work.fileIndex)
87
+ ? work.fileIndex : null;
88
+ const opening = Number.isInteger(handoffFileIndex) && this.#viewers.forSource(work.sourceKey).some(viewer =>
89
+ (work.ownerId === undefined || viewer.id === work.ownerId) && viewer.source.selectedFileIndex === handoffFileIndex &&
90
+ viewer.outputs.size > 0 && !this.#playbackAppliedFor(work.sourceKey, handoffFileIndex));
83
91
  if (work.selected && work.statement === "packets" &&
84
- work.positions !== this.#positionsFor(work.sourceKey, work.fileIndex, work.ownerId)) return null;
92
+ work.positions !== this.#positionsFor(work.sourceKey, work.fileIndex, work.ownerId, opening)) return null;
85
93
  if (work.role === "subtitle-embedded" && ["packets", "subtitle-cues"].includes(work.statement) &&
86
94
  work.positions !== this.#positionsFor(work.sourceKey, work.fileIndex, work.ownerId, true)) return null;
87
95
  const owners = this.#viewers.forSource(work.sourceKey).filter(viewer => {
@@ -90,9 +98,9 @@ export class SourcePreparation {
90
98
  viewer.subtitle.trackIndex !== work.trackIndex)) return false;
91
99
  const selected = viewer.source.selectedFileIndex;
92
100
  if (work.ownerFileIndex !== undefined) return selected === work.ownerFileIndex &&
93
- (work.role !== "source-rest" || viewer.outputs.size === 0);
101
+ (work.role !== "source-rest" || viewer.outputs.size === 0 || opening);
94
102
  if (work.statement !== "packets") return selected === null || selected === work.fileIndex;
95
- if (selected !== null) return selected === work.fileIndex && viewer.outputs.size === 0 && work.selected;
103
+ if (selected !== null) return selected === work.fileIndex && (viewer.outputs.size === 0 || opening) && work.selected;
96
104
  return !work.selected && viewer.source.visibleFileIndices.includes(work.fileIndex);
97
105
  });
98
106
  if (!owners.length) return null;