@torrent-tv/proxy 2.98.1 → 2.98.3

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.98.1",
3
+ "version": "2.98.3",
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
@@ -437,44 +437,55 @@ export async function startProxyServer({
437
437
  /** Source byte ranges of one segment interval, by file and interval; forgotten with the source. */
438
438
  const segmentSourceRanges = new Map();
439
439
  /**
440
- * What one segment of an output reads — its tracks, its interval and the key
441
- * its bytes are kept under — by output and segment, until the tracks, the
442
- * file's start time, the segment's interval or the choices of the output
443
- * change; forgotten with the source.
440
+ * WHAT ONE SEGMENT OF AN OUTPUT IS, kept from pass to pass: where it lies in
441
+ * the source, what it reads — its tracks and the key its bytes are kept under
442
+ * — and, once known, those bytes joined at the precision the torrent fetches,
443
+ * by output and segment. Worked out again only when the output's timeline,
444
+ * the tracks, the file's start time or the output's choices change; forgotten
445
+ * with the source. None of it depends on where a viewer stands, and a pass is
446
+ * made every time one moves: looked up, made and joined again for each of a
447
+ * film's 3500 segments on every pass, they were 6 s of the 40 s the passes
448
+ * cost the main thread in fifteen minutes on Home Assistant (r30,
449
+ * torrent-tv/meta#178).
444
450
  */
445
451
  const segmentReadings = new Map();
446
- // Per output, then per segment: every segment of an output of one pass shares
447
- // its list of track choices, so the output's key and the choices' key are
448
- // worked out once for it. Built for each of a pass's segments, the two
449
- // strings cost the main thread 13 s in ten minutes of a cold AVI on Home
450
- // Assistant (torrent-tv/meta#178).
451
- const readingsByChoices = new WeakMap();
452
- const segmentReadingOf = (zone, trackList, shift, map) => {
453
- let output = readingsByChoices.get(zone.tracks);
454
- if (!output) {
455
- const outputKey = `${map.sourceKey}:${map.fileIndex}:${zone.outputKey}`;
456
- let byIndex = segmentReadings.get(outputKey);
457
- if (!byIndex) segmentReadings.set(outputKey, byIndex = new Map());
458
- output = { byIndex, choicesKey: zone.tracks.map(choice => `${choice.type}/${choice.index}/${choice.mode}`).join(",") };
459
- readingsByChoices.set(zone.tracks, output);
460
- }
461
- const { byIndex, choicesKey } = output;
462
- const kept = byIndex.get(zone.index);
463
- if (kept && kept.trackList === trackList && kept.shift === shift && kept.choicesKey === choicesKey &&
464
- kept.from === zone.sourceInterval.from && kept.to === zone.sourceInterval.to) return kept.reading;
465
- const selected = zone.tracks.map(choice => ({ choice, track: choice.type === "video"
452
+ /** One output's segments, by index. */
453
+ const segmentRecordsOf = (map, output) => {
454
+ const key = `${map.sourceKey}:${map.fileIndex}:${output.outputKey}`;
455
+ let records = segmentReadings.get(key);
456
+ if (!records) segmentReadings.set(key, records = new Map());
457
+ return records;
458
+ };
459
+ /** The choices of one output's tracks as a key, worked out once for the list of them a pass shares. */
460
+ const choicesKeys = new WeakMap();
461
+ const choicesKeyOf = (choices) => {
462
+ let key = choicesKeys.get(choices);
463
+ if (key === undefined) choicesKeys.set(choices, key = choices.map(choice => `${choice.type}/${choice.index}/${choice.mode}`).join(","));
464
+ return key;
465
+ };
466
+ const segmentRecordOf = (records, demand, { timeline, trackList, shift, map }) => {
467
+ const choicesKey = choicesKeyOf(demand.tracks);
468
+ const kept = records.get(demand.index);
469
+ if (kept && kept.timeline === timeline && kept.trackList === trackList && kept.shift === shift && kept.choicesKey === choicesKey) return kept;
470
+ const sourceInterval = timeline.sourceInterval(demand.index, demand.index, shift);
471
+ const selected = demand.tracks.map(choice => ({ choice, track: choice.type === "video"
466
472
  ? ContainerTrack.firstUsable(trackList, "video")
467
473
  : trackList.filter(track => track.type === choice.type)[choice.index] }));
468
474
  let reading = null;
469
475
  if (!selected.some(({ track }) => !track)) {
470
476
  const wanted = selected.map(({ track }) => track);
471
477
  const modes = new Map(selected.map(({ choice, track }) => [track, choice.mode]));
472
- const interval = { ...zone.sourceInterval, trackIds: wanted.map(track => track.trackNumber),
478
+ const interval = { ...sourceInterval, trackIds: wanted.map(track => track.trackNumber),
473
479
  modes: Object.fromEntries(wanted.map(track => [track.trackNumber, modes.get(track)])) };
474
480
  reading = { wanted, modes, interval, rangesKey: `${map.sourceKey}:${map.fileIndex}:${JSON.stringify(interval)}` };
475
481
  }
476
- byIndex.set(zone.index, { trackList, shift, choicesKey, from: zone.sourceInterval.from, to: zone.sourceInterval.to, reading });
477
- return reading;
482
+ // `joined` is the segment's ranges as the torrent is told them, made from
483
+ // the list `ranges` at `pieceLength`; `registered` is the list the input
484
+ // table was last given.
485
+ const record = { timeline, trackList, shift, choicesKey, sourceInterval, reading,
486
+ ranges: null, pieceLength: Number.NaN, joined: null, registered: null };
487
+ records.set(demand.index, record);
488
+ return record;
478
489
  };
479
490
  /**
480
491
  * The last complete pass over each file, by file: what it was worked out
@@ -508,15 +519,6 @@ export async function startProxyServer({
508
519
  `${passCosts.built} zone list(s) built for the map in ${Math.round(passCosts.buildMs)}ms`);
509
520
  Object.assign(passCosts, { since: now, reused: 0, worked: 0, incomplete: 0, segments: 0, ranges: 0, zones: 0, built: 0, buildMs: 0, ms: 0, maxMs: 0 });
510
521
  };
511
- /** A segment's ranges joined at the precision the torrent fetches, once per kept list and piece length. */
512
- const joinedRanges = new WeakMap();
513
- const joinedRangesOf = (ranges, pieceLength) => {
514
- const kept = joinedRanges.get(ranges);
515
- if (kept && kept.pieceLength === pieceLength) return kept.joined;
516
- const joined = joinedWithinPieces(ranges, pieceLength);
517
- if (ranges !== null && typeof ranges === "object") joinedRanges.set(ranges, { pieceLength, joined });
518
- return joined;
519
- };
520
522
  const downloadMaps = new DownloadMaps({
521
523
  // The ranges a picture reads at open are named again by every segment;
522
524
  // only the zone the swarm would rank first is sent (torrent-tv/meta#166).
@@ -574,14 +576,22 @@ export async function startProxyServer({
574
576
  const links = consumers.map(viewer => viewer.linkReading()?.linkMbps)
575
577
  .filter(value => Number.isFinite(value) && value > 0);
576
578
  const encodeSpeed = outputParts.encodeCost.latestSpeedReadingOf(output)?.speed;
579
+ const linkMbps = links.length ? Math.min(...links) : null;
580
+ const records = segmentRecordsOf(map, output);
581
+ const facts = { timeline: output.timeline, trackList: tracks.value, shift, map };
582
+ // Each demand is this pass's own object (`segmentDemands`), so it is
583
+ // completed in place rather than copied.
577
584
  for (const demand of segmentDemands(output, map.fileIndex, map.zones)) {
578
585
  if (inventory.has(demand.index)) continue;
579
586
  const deadlineAt = sourceDeadline(demand, { encodeSpeed,
580
- outputBytes: seconds > 0 ? bytes / seconds * (demand.to - demand.from) : null,
581
- linkMbps: links.length ? Math.min(...links) : null });
582
- const leadSeconds = Number.isFinite(demand.deadlineAt) ? Math.max(0, (demand.deadlineAt - deadlineAt) / 1000) : 0;
583
- demands.push({ ...demand, deadlineAt, leadSeconds, outputKey: output.outputKey,
584
- sourceInterval: output.timeline.sourceInterval(demand.index, demand.index, shift) });
587
+ outputBytes: seconds > 0 ? bytes / seconds * (demand.to - demand.from) : null, linkMbps });
588
+ const record = segmentRecordOf(records, demand, facts);
589
+ demand.leadSeconds = Number.isFinite(demand.deadlineAt) ? Math.max(0, (demand.deadlineAt - deadlineAt) / 1000) : 0;
590
+ demand.deadlineAt = deadlineAt;
591
+ demand.outputKey = output.outputKey;
592
+ demand.sourceInterval = record.sourceInterval;
593
+ demand.record = record;
594
+ demands.push(demand);
585
595
  }
586
596
  }
587
597
  demands.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
@@ -618,24 +628,37 @@ export async function startProxyServer({
618
628
  // two-hour AVI (torrent-tv/meta#178).
619
629
  const waiting = new Set();
620
630
  const turn = takeTurns();
631
+ /** One download interval per map zone, shared by the segments it owns: read, never changed. */
632
+ const downloadIntervals = new Map();
633
+ const downloadIntervalOf = (owner) => {
634
+ let interval = downloadIntervals.get(owner);
635
+ if (!interval) downloadIntervals.set(owner, interval = { from: owner.from, to: owner.to });
636
+ return interval;
637
+ };
621
638
  for (const zone of demands) {
622
639
  if (!map.isCurrent()) return [];
623
- // WHAT A SEGMENT READS IS WORKED OUT ONCE, not on every publication: its
624
- // tracks, its interval and the key its bytes are kept under depend on
625
- // the output, the segment and the file's start time, none of which a
626
- // new map changes. Rebuilt for every segment of the film on every map,
627
- // this pass held the main thread up to 1.5 s at a time on Home
640
+ // WHAT A SEGMENT READS IS WORKED OUT ONCE, not on every publication
641
+ // (`segmentRecordOf`). Rebuilt for every segment of the film on every
642
+ // map, this pass held the main thread up to 1.5 s at a time on Home
628
643
  // Assistant (torrent-tv/meta#178).
629
- const reading = segmentReadingOf(zone, tracks.value, shift, map);
644
+ const { record } = zone;
645
+ const reading = record.reading;
630
646
  if (!reading) continue;
631
647
  const { wanted, modes, interval, rangesKey } = reading;
632
- const { tracks: _choices, owner: _owner, sourceInterval: _sourceInterval, ...demand } = zone;
633
648
  const convert = ranges => {
634
- const joined = joinedRangesOf(ranges, pieceLength);
649
+ if (record.ranges !== ranges || record.pieceLength !== pieceLength) {
650
+ record.ranges = ranges;
651
+ record.pieceLength = pieceLength;
652
+ record.joined = joinedWithinPieces(ranges, pieceLength);
653
+ }
654
+ const joined = record.joined;
635
655
  rangeCount += joined.length;
636
656
  // What the segment reads is kept for the forecast in a table of its
637
657
  // own, whatever form the torrent is told it in.
638
- segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
658
+ if (record.registered !== joined) {
659
+ segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
660
+ record.registered = joined;
661
+ }
639
662
  // ONE ZONE FOR THE SEGMENT, its demand stated once for all the
640
663
  // ranges it reads. The demand — level, priority, deadline, the map
641
664
  // zone it belongs to — is this pass's; the ranges are the list kept
@@ -643,14 +666,16 @@ export async function startProxyServer({
643
666
  // range copied the demand into every one: ten thousand objects a
644
667
  // pass over a two-hour AVI, made again 21-61 times a minute on Home
645
668
  // Assistant (r26, torrent-tv/meta#178).
646
- converted.push({ ...demand, downloadInterval: { from: zone.owner.from, to: zone.owner.to }, ranges: joined });
669
+ converted.push({ index: zone.index, from: zone.from, to: zone.to, priority: zone.priority, urgent: zone.urgent,
670
+ behind: zone.behind, deferred: zone.deferred, deadlineAt: zone.deadlineAt, leadSeconds: zone.leadSeconds,
671
+ outputKey: zone.outputKey, downloadInterval: downloadIntervalOf(zone.owner), ranges: joined });
647
672
  };
648
673
  // The bytes one segment needs are a fact of the file and the output, so
649
674
  // they are worked out once. A whole film is thousands of segments and
650
675
  // the map changes every few seconds; worked out afresh on every change,
651
676
  // a pass over a two-hour AVI never finished and the next segment's bytes
652
677
  // were never asked for (Home Assistant 2026-10-08, torrent-tv/meta#151).
653
- const known = segmentSourceRanges.get(rangesKey);
678
+ const known = record.ranges ?? segmentSourceRanges.get(rangesKey);
654
679
  if (known) { convert(known); continue; }
655
680
  if (waiting.has(zone.outputKey)) { complete = false; continue; }
656
681
  // What is resolved so far is stated before a lower zone is worked out
@@ -54,20 +54,56 @@ export async function* matroskaInput({ tracks, readPacket, originSeconds = 0, in
54
54
  slices = avcPacketSlices(slices);
55
55
  payloadBytes = slices.reduce((sum, slice) => sum + slice.length, 0);
56
56
  }
57
- const blockHeader = Buffer.concat([vint(selected + 1), Buffer.from([0, 0, 0])]);
57
+ // EVERY PACKET'S CLUSTER IS WRITTEN INTO ONE BUFFER, its sizes worked out
58
+ // first: an element at a time, each id parsed from hex, each number split
59
+ // into bytes through BigInt and every part joined again, this writer was
60
+ // 6 % of the main thread over a two-hour AVI on Home Assistant (r30,
61
+ // torrent-tv/meta#178). The bytes are the same.
58
62
  const duration = Math.round(packet.duration * 1e6);
59
- const afterBlock = Buffer.concat([
60
- ...(duration > 0 ? [uintElement("9b", duration)] : []),
61
- ...(packet.discardPaddingSeconds ? [signedElement("75a2", Math.round(packet.discardPaddingSeconds * 1e9))] : []),
62
- ...(packet.keyframe ? [] : [element("fb", Buffer.from([0]))])]);
63
- const blockPrefix = prefix("a1", blockHeader.length + payloadBytes);
64
- const groupPrefix = prefix("a0", blockPrefix.length + blockHeader.length + payloadBytes + afterBlock.length);
65
- const timestamp = uintElement("e7", stamp);
66
- const clusterPrefix = prefix("1f43b675", timestamp.length + groupPrefix.length + blockPrefix.length +
67
- blockHeader.length + payloadBytes + afterBlock.length);
68
- yield Buffer.concat([clusterPrefix, timestamp, groupPrefix, blockPrefix, blockHeader]);
63
+ const padding = packet.discardPaddingSeconds ? Math.round(packet.discardPaddingSeconds * 1e9) : null;
64
+ if (duration > 0 && !Number.isSafeInteger(duration)) throw new TypeError("EBML unsigned integer is invalid.");
65
+ if (padding !== null && !Number.isSafeInteger(padding)) throw new TypeError("EBML signed integer is outside its exact range.");
66
+ const blockHeaderBytes = vintWidth(selected + 1) + 3;
67
+ const durationWidth = duration > 0 ? uintWidth(duration) : 0;
68
+ const afterBytes = (duration > 0 ? 1 + vintWidth(durationWidth) + durationWidth : 0) +
69
+ (padding !== null ? 2 + 1 + 8 : 0) + (packet.keyframe ? 0 : 3);
70
+ const blockSize = blockHeaderBytes + payloadBytes;
71
+ const blockPrefixBytes = 1 + vintWidth(blockSize);
72
+ const groupSize = blockPrefixBytes + blockSize + afterBytes;
73
+ const groupPrefixBytes = 1 + vintWidth(groupSize);
74
+ const stampWidth = uintWidth(stamp);
75
+ const timestampBytes = 1 + vintWidth(stampWidth) + stampWidth;
76
+ const clusterSize = timestampBytes + groupPrefixBytes + groupSize;
77
+ const head = Buffer.allocUnsafe(4 + vintWidth(clusterSize) + timestampBytes + groupPrefixBytes + blockPrefixBytes + blockHeaderBytes);
78
+ let at = head.writeUInt32BE(0x1f43b675, 0);
79
+ at = writeVint(head, at, clusterSize);
80
+ head[at++] = 0xe7;
81
+ at = writeVint(head, at, stampWidth);
82
+ at = writeUint(head, at, stamp, stampWidth);
83
+ head[at++] = 0xa0;
84
+ at = writeVint(head, at, groupSize);
85
+ head[at++] = 0xa1;
86
+ at = writeVint(head, at, blockSize);
87
+ at = writeVint(head, at, selected + 1);
88
+ head[at++] = 0; head[at++] = 0; head[at++] = 0;
89
+ yield head;
69
90
  for (const slice of slices) yield slice;
70
- if (afterBlock.length) yield afterBlock;
91
+ if (afterBytes > 0) {
92
+ const after = Buffer.allocUnsafe(afterBytes);
93
+ let offset = 0;
94
+ if (duration > 0) {
95
+ after[offset++] = 0x9b;
96
+ offset = writeVint(after, offset, durationWidth);
97
+ offset = writeUint(after, offset, duration, durationWidth);
98
+ }
99
+ if (padding !== null) {
100
+ offset = after.writeUInt16BE(0x75a2, offset);
101
+ offset = writeVint(after, offset, 8);
102
+ offset = after.writeBigInt64BE(BigInt(padding), offset);
103
+ }
104
+ if (!packet.keyframe) { after[offset++] = 0xfb; after[offset++] = 0x81; after[offset++] = 0; }
105
+ yield after;
106
+ }
71
107
  }
72
108
  }
73
109
 
@@ -128,31 +164,52 @@ function matroskaCodec(codec) {
128
164
  return declared;
129
165
  }
130
166
 
131
- function vint(value) {
167
+ /** How many bytes an EBML size of this value takes: the narrowest whose all-ones value it is below. */
168
+ function vintWidth(value) {
132
169
  if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("EBML value is outside its integer range.");
133
- for (let width = 1; width <= 8; width++) {
134
- if (BigInt(value) >= (1n << BigInt(7 * width)) - 1n) continue;
135
- const encoded = Buffer.alloc(width);
136
- let bits = BigInt(value) | (1n << BigInt(7 * width));
137
- for (let at = width - 1; at >= 0; at--) { encoded[at] = Number(bits & 255n); bits >>= 8n; }
138
- return encoded;
170
+ // 2 ** 56 - 1 is above every safe integer, so eight bytes always hold one.
171
+ for (let width = 1; width < 8; width++) if (value < 2 ** (7 * width) - 1) return width;
172
+ return 8;
173
+ }
174
+
175
+ /** The value as an EBML size at `at`, its width's marker bit set; the next offset. */
176
+ function writeVint(target, at, value) {
177
+ const width = vintWidth(value);
178
+ writeUint(target, at, value, width);
179
+ target[at] |= 1 << (8 - width);
180
+ return at + width;
181
+ }
182
+
183
+ /** How many bytes an unsigned integer takes, one at least. */
184
+ function uintWidth(value) {
185
+ let width = 1;
186
+ for (let rest = Math.floor(value / 256); rest > 0; rest = Math.floor(rest / 256)) width += 1;
187
+ return width;
188
+ }
189
+
190
+ /** The value big-endian in `width` bytes at `at`; the next offset. */
191
+ function writeUint(target, at, value, width) {
192
+ let rest = value;
193
+ for (let index = width - 1; index >= 0; index--) {
194
+ target[at + index] = rest % 256;
195
+ rest = Math.floor(rest / 256);
139
196
  }
140
- throw new TypeError("EBML value has no valid width.");
197
+ return at + width;
198
+ }
199
+
200
+ function vint(value) {
201
+ const encoded = Buffer.alloc(vintWidth(value));
202
+ writeVint(encoded, 0, value);
203
+ return encoded;
141
204
  }
142
205
 
143
206
  function prefix(id, size) { return Buffer.concat([Buffer.from(id, "hex"), vint(size)]); }
144
207
  function element(id, payload) { return Buffer.concat([prefix(id, payload.length), payload]); }
145
208
  function textElement(id, text) { return element(id, Buffer.from(text, "utf8")); }
146
- function signedElement(id, value) {
147
- if (!Number.isSafeInteger(value)) throw new TypeError("EBML signed integer is outside its exact range.");
148
- const bytes = Buffer.alloc(8);
149
- bytes.writeBigInt64BE(BigInt(value));
150
- return element(id, bytes);
151
- }
152
209
  function uintElement(id, value) {
153
210
  if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("EBML unsigned integer is invalid.");
154
- let bits = BigInt(value);
155
- const bytes = [];
156
- do { bytes.unshift(Number(bits & 255n)); bits >>= 8n; } while (bits > 0n);
157
- return element(id, Buffer.from(bytes));
211
+ const width = uintWidth(value);
212
+ const bytes = Buffer.alloc(width);
213
+ writeUint(bytes, 0, value, width);
214
+ return element(id, bytes);
158
215
  }
@@ -22,18 +22,30 @@
22
22
  * behind?: boolean, deferred?: boolean, from: number, to: number,
23
23
  * sourceInterval?: { from: number, to: number } }>} params.demands - In the
24
24
  * order of their deadlines.
25
- * @returns {{ outputs: string[], inputs: number[] }}
25
+ * @returns {{ outputs: string[], inputs: Float64Array }}
26
26
  */
27
27
  export function passInputs({ pieceLength, shift, targets, demands }) {
28
28
  const outputs = [...targets.keys()];
29
- const inputs = [pieceLength, shift];
30
- for (const target of targets.values()) inputs.push(target ?? -1);
29
+ const position = new Map(outputs.map((outputKey, at) => [outputKey, at]));
30
+ // Numbers in a list made to its length: grown a value at a time, eight for
31
+ // each of a film's 3500 segments, it was most of what comparing a pass cost
32
+ // (r30, torrent-tv/meta#178).
33
+ const inputs = new Float64Array(2 + targets.size + demands.length * 8);
34
+ let at = 0;
35
+ inputs[at++] = pieceLength;
36
+ inputs[at++] = shift;
37
+ for (const target of targets.values()) inputs[at++] = target ?? -1;
31
38
  for (const demand of demands) {
32
39
  // The segment's own interval too: a cut grid read again can give one
33
40
  // number other bytes.
34
- inputs.push(outputs.indexOf(demand.outputKey), demand.index, demand.priority,
35
- (demand.urgent ? 1 : 0) + (demand.behind ? 2 : 0) + (demand.deferred ? 4 : 0),
36
- demand.from, demand.to, demand.sourceInterval?.from ?? -1, demand.sourceInterval?.to ?? -1);
41
+ inputs[at++] = position.get(demand.outputKey) ?? -1;
42
+ inputs[at++] = demand.index;
43
+ inputs[at++] = demand.priority;
44
+ inputs[at++] = (demand.urgent ? 1 : 0) + (demand.behind ? 2 : 0) + (demand.deferred ? 4 : 0);
45
+ inputs[at++] = demand.from;
46
+ inputs[at++] = demand.to;
47
+ inputs[at++] = demand.sourceInterval?.from ?? -1;
48
+ inputs[at++] = demand.sourceInterval?.to ?? -1;
37
49
  }
38
50
  return { outputs, inputs };
39
51
  }
@@ -41,8 +53,8 @@ export function passInputs({ pieceLength, shift, targets, demands }) {
41
53
  /**
42
54
  * Whether a pass is worked out from what an earlier one was.
43
55
  *
44
- * @param {{ tracks: unknown, outputs: string[], inputs: number[] } | undefined} last
45
- * @param {{ tracks: unknown, outputs: string[], inputs: number[] }} next
56
+ * @param {{ tracks: unknown, outputs: string[], inputs: ArrayLike<number> } | undefined} last
57
+ * @param {{ tracks: unknown, outputs: string[], inputs: ArrayLike<number> }} next
46
58
  * @returns {boolean}
47
59
  */
48
60
  export function samePass(last, next) {
@@ -22,28 +22,27 @@ export function segmentDemands(output, fileIndex, zones) {
22
22
  for (let index = 0; index + 1 < grid.length; index++) {
23
23
  const from = grid[index], to = grid[index + 1];
24
24
  if (!Number.isFinite(from) || !Number.isFinite(to) || !(to > from)) throw new Error("An output segment needs a valid interval.");
25
- let overlapping;
26
- if (ordered) {
27
- while (first < zones.length && zones[first].to <= from) first += 1;
28
- overlapping = [];
29
- for (let at = first; at < zones.length && zones[at].from < to; at += 1) {
30
- if (zones[at].priority > 0) overlapping.push(zones[at]);
31
- }
32
- } else {
33
- overlapping = zones.filter(zone => zone.priority > 0 && zone.from < to && zone.to > from);
25
+ if (ordered) while (first < zones.length && zones[first].to <= from) first += 1;
26
+ // The zones a segment overlaps are folded as they are met, into numbers:
27
+ // a list of them, and the lists made of it for each field, were five
28
+ // arrays for every segment of a film on every pass (r30,
29
+ // torrent-tv/meta#178).
30
+ let owner = null, ownerDeadline = Infinity, priority = 0, urgent = false, behind = true, deferred = true;
31
+ for (let at = ordered ? first : 0; at < zones.length; at += 1) {
32
+ const zone = zones[at];
33
+ if (ordered && !(zone.from < to)) break;
34
+ if (!(zone.priority > 0) || !(zone.from < to && zone.to > from)) continue;
35
+ // A zone states the deadline of its near edge. A later segment within it
36
+ // is reached later, at one second of film per second of playback.
37
+ const deadline = Number.isFinite(zone.deadlineAt) ? zone.deadlineAt + Math.max(0, from - zone.from) * 1000 : Infinity;
38
+ if (owner === null || deadline < ownerDeadline) { owner = zone; ownerDeadline = deadline; }
39
+ if (zone.priority > priority) priority = zone.priority;
40
+ if (zone.urgent) urgent = true;
41
+ if (!zone.behind) behind = false;
42
+ if (!zone.deferred) deferred = false;
34
43
  }
35
- if (!overlapping.length) continue;
36
- // A zone states the deadline of its near edge. A later segment within it
37
- // is reached later, at one second of film per second of playback.
38
- const deadlineOf = zone => Number.isFinite(zone.deadlineAt)
39
- ? zone.deadlineAt + Math.max(0, from - zone.from) * 1000 : Infinity;
40
- const owner = overlapping.reduce((first, zone) => deadlineOf(zone) < deadlineOf(first) ? zone : first);
41
- demands.push({ index, from, to, tracks, owner,
42
- priority: Math.max(...overlapping.map(zone => zone.priority)),
43
- urgent: overlapping.some(zone => zone.urgent),
44
- behind: overlapping.every(zone => zone.behind),
45
- deferred: overlapping.every(zone => zone.deferred),
46
- deadlineAt: Math.min(...overlapping.map(deadlineOf)) });
44
+ if (owner === null) continue;
45
+ demands.push({ index, from, to, tracks, owner, priority, urgent, behind, deferred, deadlineAt: ownerDeadline });
47
46
  }
48
47
  return demands.sort((left, right) => left.deadlineAt - right.deadlineAt || right.priority - left.priority || left.index - right.index);
49
48
  }