@torrent-tv/proxy 2.107.6 → 2.107.7

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.107.6",
3
+ "version": "2.107.7",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -1,7 +1,8 @@
1
1
  import { Container } from "./Container.js";
2
2
  import { VideoTrack } from "../tracks/VideoTrack.js";
3
3
  import { AudioTrack } from "../tracks/AudioTrack.js";
4
- import { readAsfPackets } from "./asf-packets.js";
4
+ import { readAsfData, readAsfPacketTime, readAsfPackets, readAsfWindow, WINDOW_LEAD_SECONDS, WINDOW_SEARCH_SECONDS } from "./asf-packets.js";
5
+ import { BytesUnavailable, isUnavailable } from "./unavailable.js";
5
6
  import { RetainedReads } from "./RetainedReads.js";
6
7
 
7
8
  const HEADER = "3026b2758e66cf11a6d900aa0062ce6c";
@@ -28,6 +29,10 @@ export class AsfContainer extends Container {
28
29
  #startRead = false;
29
30
  #duration = null;
30
31
  #declarations = null;
32
+ /** The fixed-length data packets, false where packets cannot be found by number, null before the Data Object is read. */
33
+ #data = null;
34
+ /** Send times of the packets read by number. @type {Map<number, number>} */
35
+ #sent = new Map();
31
36
 
32
37
  get formatName() { return "asf"; }
33
38
  packetIndexBytes() {
@@ -197,8 +202,12 @@ export class AsfContainer extends Container {
197
202
  const track = this.#tracks.find(track => track.type === "video");
198
203
  return track ? { times: index.keyframesOf(track.trackNumber), tolerance: 0 } : null;
199
204
  }
200
- async readPacketIndex() {
205
+ async readPacketIndex(interval) {
201
206
  await this.#readHeader();
207
+ if (!this.#packets && Number.isFinite(interval?.from) && Number.isFinite(interval?.to) && interval.to > interval.from) {
208
+ const window = await this.#readWindow(interval);
209
+ if (window) return window;
210
+ }
202
211
  if (!this.#packets) {
203
212
  const index = await readAsfPackets({ readRange: this.readRange, fileSize: this.fileSize, tracks: this.#tracks,
204
213
  info: { ...this.#info, durationSeconds: this.#duration }, state: this.#packetState, packetMemory: this.packetMemory });
@@ -214,4 +223,106 @@ export class AsfContainer extends Container {
214
223
  }
215
224
  return this.#packets;
216
225
  }
226
+
227
+ /**
228
+ * The packets of one interval, from a window of the Data Object around it
229
+ * (`readAsfWindow`), or null where its packets cannot be found by number.
230
+ *
231
+ * A payload is presented no earlier than it is sent, and in the files
232
+ * measured a preroll after it (FFmpeg's muxer, 2026-10-23): so the window
233
+ * begins at a packet sent a preroll and `WINDOW_LEAD_SECONDS` before the
234
+ * interval, or up to `WINDOW_SEARCH_SECONDS` earlier. A window that does
235
+ * not reach back to a keyframe of the picture, or to the sound before the
236
+ * interval, is read again from twice as far back, and from the first packet
237
+ * at the latest; which it reaches is checked on what it read, not assumed.
238
+ */
239
+ async #readWindow(interval) {
240
+ const data = await this.#dataLayout();
241
+ if (!data) return null;
242
+ const requested = this.#tracks.filter(track => ["video", "audio"].includes(track.type) &&
243
+ (!interval.trackIds?.length || interval.trackIds.includes(track.trackNumber)));
244
+ if (!requested.length) return null;
245
+ const info = { ...this.#info, durationSeconds: this.#duration };
246
+ for (let lead = this.#info.prerollSeconds + WINDOW_LEAD_SECONDS; ; lead *= 2) {
247
+ const sentFrom = interval.from - lead;
248
+ const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS);
249
+ const window = await readAsfWindow({ readRange: this.readRange, data, tracks: this.#tracks, requested, info, first, interval, sentFrom });
250
+ if (window.startCovered || first === 0) return window.index;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * The Data Object's packets, when each has the length the file declares:
256
+ * the format requires it (the File Properties state one length as both the
257
+ * least and the most), and a packet is then found by its number.
258
+ */
259
+ async #dataLayout() {
260
+ if (this.#data === null) {
261
+ const data = await readAsfData({ readRange: this.readRange, fileSize: this.fileSize, info: this.#info });
262
+ const length = this.#info.packetLength;
263
+ const fixed = length > 0 && this.#info.minimumPacketLength === length;
264
+ const whole = fixed ? Math.floor((data.end - data.start) / length) : 0;
265
+ const count = data.count > 0 ? Math.min(data.count, whole) : whole;
266
+ this.#data = count > 0 ? { ...data, count, length } : false;
267
+ }
268
+ return this.#data || null;
269
+ }
270
+
271
+ /**
272
+ * The number of a packet sent at or before `seconds` and no more than
273
+ * `WINDOW_SEARCH_SECONDS` before it, or of the first packet when that is
274
+ * sent later. Packets are stored in the order they are sent, so the number
275
+ * is guessed between the packets already read on either side, by the file's
276
+ * rate between them, and aimed half that margin early so that a guess near
277
+ * the mark is the answer: a packet is read only where the search stands.
278
+ * After three guesses the search halves what is left.
279
+ *
280
+ * A guessed packet that is not held is said as missing together with the
281
+ * bytes the window from it to `until` is estimated to take, so the swarm is
282
+ * asked for the packet and the window it begins at once.
283
+ */
284
+ async #packetSentBefore(seconds, data, until) {
285
+ let low = 0, high = data.count, lowSent = await this.#sentAt(0, data), highSent = this.#duration;
286
+ if (lowSent > seconds) return 0;
287
+ // Past the last packet nothing is sent before the film ends; the last
288
+ // packet itself is read only when the search needs it.
289
+ if (!(highSent > seconds)) {
290
+ high = data.count - 1;
291
+ highSent = await this.#sentAt(high, data);
292
+ if (highSent <= seconds) return high;
293
+ }
294
+ for (const [number, at] of this.#sent) {
295
+ if (number <= low || number >= high) continue;
296
+ if (at <= seconds) { low = number; lowSent = at; }
297
+ else { high = number; highSent = at; }
298
+ }
299
+ for (let guesses = 0; high - low > 1 && seconds - lowSent > WINDOW_SEARCH_SECONDS; guesses++) {
300
+ const perSecond = (high - low) / (highSent - lowSent);
301
+ const aim = seconds - WINDOW_SEARCH_SECONDS / 2;
302
+ const guess = guesses >= 3 || !(perSecond > 0) ? Math.floor((low + high) / 2) : low + Math.floor((aim - lowSent) * perSecond);
303
+ const probe = Math.min(high - 1, Math.max(low + 1, guess));
304
+ const window = perSecond > 0 ? Math.ceil((until - aim) * perSecond) : 1;
305
+ const at = await this.#sentAt(probe, data, Math.max(1, window));
306
+ if (at <= seconds) { low = probe; lowSent = at; }
307
+ else { high = probe; highSent = at; }
308
+ }
309
+ return low;
310
+ }
311
+
312
+ /**
313
+ * When packet `number` is sent. A packet that is not held is said as
314
+ * missing with the `packets` packets from it.
315
+ */
316
+ async #sentAt(number, data, packets = 1) {
317
+ if (!this.#sent.has(number)) {
318
+ const at = data.start + number * data.length;
319
+ try {
320
+ this.#sent.set(number, await readAsfPacketTime({ readRange: this.readRange, at, end: data.end, info: this.#info }));
321
+ } catch (error) {
322
+ if (!isUnavailable(error) || packets <= 1) throw error;
323
+ throw new BytesUnavailable(at, Math.min(data.start + data.count * data.length, at + packets * data.length) - 1, 0);
324
+ }
325
+ }
326
+ return this.#sent.get(number);
327
+ }
217
328
  }
@@ -2,12 +2,12 @@ import { PacketIndex } from "./PacketIndex.js";
2
2
  import { PacketRecords } from "./PacketRecords.js";
3
3
  import { followingPacketTime } from "./following-packet-time.js";
4
4
  import { isMp3Codec } from "./mp3-packet-dependencies.js";
5
+ import { BytesUnavailable, isUnavailable } from "./unavailable.js";
5
6
 
6
7
  const DATA = "3626b2758e66cf11a6d900aa0062ce6c";
7
8
 
8
- /** ASF payload addresses, including fragmented and compressed media objects. */
9
- export async function readAsfPackets({ readRange, fileSize, tracks, info, firstPicture = false, packetMemory, state = {} }) {
10
- if (!state.initialized) {
9
+ /** The Data Object's header: where its packets begin and end, and how many it declares. */
10
+ export async function readAsfData({ readRange, fileSize, info }) {
11
11
  const at = info.dataOffset;
12
12
  const data = await readRange(at, at + 49);
13
13
  if (data.subarray(0, 16).toString("hex") !== DATA) throw new Error("ASF Data Object is absent.");
@@ -16,7 +16,14 @@ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstP
16
16
  if (!Number.isSafeInteger(size) || size < 50 || at + size > fileSize || !Number.isSafeInteger(count)) {
17
17
  throw new Error("ASF Data Object exceeds the file.");
18
18
  }
19
- Object.assign(state, { initialized: true, at: at + 50, end: at + size, count, packets: 0,
19
+ return { start: at + 50, end: at + size, count };
20
+ }
21
+
22
+ /** ASF payload addresses, including fragmented and compressed media objects. */
23
+ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstPicture = false, packetMemory, state = {} }) {
24
+ if (!state.initialized) {
25
+ const data = await readAsfData({ readRange, fileSize, info });
26
+ Object.assign(state, { initialized: true, at: data.start, end: data.end, count: data.count, packets: 0,
20
27
  frames: new Map(tracks.map(track => [track.trackNumber, new PacketRecords(packetMemory)])), assembling: new Map() });
21
28
  }
22
29
  let { at, packets } = state;
@@ -26,78 +33,7 @@ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstP
26
33
  const previousObjects = new Map([...assembling].map(([id, object]) => [id, { ...object, ranges: [...object.ranges] }]));
27
34
  const previousLengths = new Map([...frames].map(([id, stream]) => [id, stream.length]));
28
35
  try {
29
- const fields = new Fields(readRange, at, end);
30
- let flags = await fields.uint(1);
31
- if (flags & 0x80) {
32
- if (flags & 0x60) throw new Error("ASF error-correction length type is unsupported.");
33
- fields.skip(flags & 15);
34
- flags = await fields.uint(1);
35
- }
36
- const properties = await fields.uint(1);
37
- const length = await fields.sized(flags >> 5, info.packetLength);
38
- await fields.sized(flags >> 1, 0);
39
- const padding = await fields.sized(flags >> 3, 0);
40
- const timestamp = await fields.uint(4);
41
- await fields.uint(2);
42
- if (!length || length > end - at || padding >= length) throw new Error("ASF packet length or padding is invalid.");
43
- const limit = at + length - padding;
44
- fields.limit = limit;
45
- const payloadFlags = flags & 1 ? await fields.uint(1) : 0x81;
46
- const payloadCount = flags & 1 ? payloadFlags & 63 : 1;
47
- if (!payloadCount) throw new Error("ASF packet has no payloads.");
48
- for (let payload = 0; payload < payloadCount; payload++) {
49
- const stream = await fields.uint(1);
50
- const id = stream & 127, keyframe = (stream & 128) !== 0;
51
- if (!declared.has(id)) throw new Error("ASF payload names an undeclared stream.");
52
- const number = await fields.sized(properties >> 4, 0);
53
- const offset = await fields.sized(properties >> 2, 0);
54
- const replicaLength = await fields.sized(properties, 0);
55
- let objectSize = null, time = timestamp, delta = null;
56
- if (replicaLength >= 8) {
57
- objectSize = await fields.uint(4);
58
- time = await fields.uint(4);
59
- fields.skip(replicaLength - 8);
60
- } else if (replicaLength === 1) {
61
- time = offset;
62
- delta = await fields.uint(1);
63
- } else if (replicaLength !== 0) throw new Error("ASF replicated payload data is invalid.");
64
- const payloadSize = flags & 1 ? await fields.sized(payloadFlags >> 6, 0) : limit - fields.at;
65
- if (!payloadSize || payloadSize > limit - fields.at) throw new Error("ASF payload exceeds its packet.");
66
- const payloadEnd = fields.at + payloadSize;
67
- if (delta !== null) {
68
- while (fields.at < payloadEnd) {
69
- const frameSize = await fields.uint(1);
70
- if (!frameSize || frameSize > payloadEnd - fields.at) throw new Error("ASF compressed frame exceeds its payload.");
71
- frames.get(id).push({ pts: time / 1000 - info.prerollSeconds, ...(delta > 0 ? { duration: delta / 1000 } : {}),
72
- ...(isMp3Codec(declared.get(id)?.codecId) ? { decodeFromIndex: frames.get(id).length, decodeDependencyUnknown: true } : {}),
73
- keyframe, ranges: [[fields.at, fields.at + frameSize - 1]] });
74
- fields.skip(frameSize);
75
- time += delta;
76
- }
77
- } else {
78
- let object = assembling.get(id);
79
- if (offset === 0) {
80
- if (object) throw new Error("ASF media object ended before all fragments arrived.");
81
- if (!(objectSize > 0)) throw new Error("ASF media object size is absent.");
82
- object = { number, size: objectSize, received: 0, pts: time / 1000 - info.prerollSeconds, keyframe, ranges: [] };
83
- assembling.set(id, object);
84
- }
85
- if (!object || object.number !== number || object.received !== offset ||
86
- (objectSize !== null && object.size !== objectSize) || offset + payloadSize > object.size) {
87
- throw new Error("ASF media object fragments are incomplete or inconsistent.");
88
- }
89
- object.ranges.push([fields.at, payloadEnd - 1]);
90
- object.received += payloadSize;
91
- fields.skip(payloadSize);
92
- if (object.received === object.size) {
93
- frames.get(id).push({ pts: object.pts, keyframe: object.keyframe, ranges: object.ranges,
94
- ...(isMp3Codec(declared.get(id)?.codecId) ? { decodeFromIndex: frames.get(id).length, decodeDependencyUnknown: true } : {}) });
95
- assembling.delete(id);
96
- }
97
- }
98
- }
99
- if (fields.at !== limit) throw new Error("ASF payload count differs from its packet size.");
100
- at += Math.max(length, info.minimumPacketLength ?? length);
36
+ ({ next: at } = await readPacket({ readRange, at, end, info, declared, frames, assembling }));
101
37
  packets++;
102
38
  state.at = at;
103
39
  state.packets = packets;
@@ -119,22 +55,253 @@ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstP
119
55
  for (const track of tracks) {
120
56
  if (!["video", "audio"].includes(track.type)) continue;
121
57
  const stream = frames.get(track.trackNumber);
122
- index.declareTrack(track.trackNumber, { type: track.type, codecId: track.codecId, codecRanges: track.codecRanges ?? [],
123
- prerollSeconds: track.seekPrerollSeconds ?? 0 });
124
- const following = (track.defaultDurationSeconds === undefined || track.defaultDurationSeconds === null) && stream.some(frame => frame.duration === undefined)
125
- ? followingPacketTime(stream, info.durationSeconds, packetMemory) : null;
126
- try { for (let position = 0; position < stream.length; position++) {
127
- const stated = stream.durationAt(position), pts = stream.ptsAt(position);
128
- const duration = !Number.isNaN(stated) ? stated : track.defaultDurationSeconds ?? following(pts) - pts;
129
- if (!(duration > 0)) throw new Error("ASF frame duration cannot be determined.");
130
- if (duration !== stated) stream.setDuration(position, duration);
131
- } } finally { following?.dispose(); }
58
+ declareTrackOf(index, track);
59
+ setDurations(stream, stream.length, track, info.durationSeconds, packetMemory);
132
60
  index.sharePackets(track.trackNumber, stream);
133
61
  index.complete(track.trackNumber);
134
62
  }
135
63
  return index;
136
64
  }
137
65
 
66
+ /** The time a data packet is sent at, in seconds on its payloads' clock. */
67
+ export async function readAsfPacketTime({ readRange, at, end, info }) {
68
+ const { timestamp } = await readPacketHeader(new Fields(readRange, at, end), info.packetLength);
69
+ return timestamp / 1000 - info.prerollSeconds;
70
+ }
71
+
72
+ /**
73
+ * The packets of `interval` alone, read from the data packet numbered
74
+ * `first` on and only as far as the interval needs, without the packets
75
+ * before it or after it. The whole walk of the Data Object answered no
76
+ * interval before it had read the file to its end, so a film of this format
77
+ * had no segment with input bytes, its first included, until the whole of it
78
+ * was downloaded (torrent-tv/meta#243).
79
+ *
80
+ * Two rules of the format make that possible. Packets are stored in the order
81
+ * they are sent, and a payload is sent no later than it is presented, so once
82
+ * a packet sent at `t` is read, every payload presented before `t` has been
83
+ * read. The walk stops there once, for every track, that holds the first two
84
+ * presentation times at or after the interval's end: the frame at the first
85
+ * is the last this window keeps, and the second states its duration.
86
+ *
87
+ * What the window begins with is not known until it is read: a media object
88
+ * whose first fragment lies before `first` is passed over, and the
89
+ * answer says whether the window holds each track from early enough
90
+ * (`startCovered`): sound from `WINDOW_LEAD_SECONDS` before the interval, and
91
+ * a picture from a keyframe at or before it. If it does not, the caller reads
92
+ * the window again from an earlier packet.
93
+ *
94
+ * The packets are read a portion at a time. A portion that is not held whole
95
+ * is read a packet at a time, and a packet that is not held is said as
96
+ * missing together with the bytes the rest of the window is estimated to take
97
+ * at the file's average rate, so the swarm is asked for the window once
98
+ * rather than a piece at a time.
99
+ *
100
+ * @param {object} params
101
+ * @param {{ start: number, end: number, count: number, length: number }} params.data - The fixed-length packets.
102
+ * @param {object[]} params.requested - The tracks the interval is read for.
103
+ * @param {number} params.first - The number of the packet the window begins at.
104
+ * @param {{ from: number, to: number }} params.interval
105
+ * @param {number} params.sentFrom - When packet `first` is sent, as far as the caller knows.
106
+ * @returns {Promise<{ index: PacketIndex, startCovered: boolean }>}
107
+ */
108
+ export async function readAsfWindow({ readRange, data, tracks, requested, info, first, interval, sentFrom }) {
109
+ const declared = new Map(tracks.map(track => [track.trackNumber, track]));
110
+ // Not admitted to IndexMemory: one segment's records, a few hundred, held
111
+ // by the request that asked and dropped with it. Nothing owns a window to
112
+ // dispose of it, and admitted, each segment's would stay counted until the
113
+ // file is forgotten.
114
+ const frames = new Map(tracks.map(track => [track.trackNumber, new PacketRecords()]));
115
+ const assembling = new Map();
116
+ const started = new Set();
117
+ const ids = requested.map(track => track.trackNumber);
118
+ /** Per track, the two earliest distinct presentation times at or after the interval's end. */
119
+ const after = new Map(ids.map(id => [id, []]));
120
+ const examined = new Map(ids.map(id => [id, 0]));
121
+ const dataEnd = data.start + data.count * data.length;
122
+ const rate = info.durationSeconds > 0 ? (dataEnd - data.start) / info.durationSeconds : 0;
123
+ let at = data.start + first * data.length, sent = sentFrom, reachedEnd = false;
124
+ let buffer = null, bufferAt = 0;
125
+ const held = (start, end) => buffer !== null && start >= bufferAt && end < bufferAt + buffer.length;
126
+ const read = (start, end) => held(start, end) ? buffer.subarray(start - bufferAt, end - bufferAt + 1) : readRange(start, end);
127
+ const load = async () => {
128
+ try {
129
+ buffer = await readRange(at, Math.min(dataEnd - 1, at + WINDOW_PORTION_PACKETS * data.length - 1));
130
+ } catch (error) {
131
+ if (!isUnavailable(error)) throw error;
132
+ try {
133
+ buffer = await readRange(at, Math.min(dataEnd - 1, at + data.length - 1));
134
+ } catch (missing) {
135
+ if (!isUnavailable(missing)) throw missing;
136
+ const ahead = Math.ceil(Math.max(0, interval.to + WINDOW_LEAD_SECONDS - sent) * rate);
137
+ throw new BytesUnavailable(at, Math.min(dataEnd - 1, Math.max(at + data.length, at + ahead) - 1), 0);
138
+ }
139
+ }
140
+ bufferAt = at;
141
+ };
142
+ for (;;) {
143
+ if (at >= dataEnd) { reachedEnd = true; break; }
144
+ if (!held(at, Math.min(dataEnd, at + data.length) - 1)) await load();
145
+ ({ next: at, sent } = await readPacket({ readRange: read, at, end: data.end, info, declared, frames, assembling, started }));
146
+ for (const id of ids) {
147
+ const stream = frames.get(id), times = after.get(id);
148
+ for (let position = examined.get(id); position < stream.length; position++) {
149
+ const pts = stream.ptsAt(position);
150
+ if (pts < interval.to || times.includes(pts)) continue;
151
+ times.push(pts);
152
+ times.sort((left, right) => left - right);
153
+ times.length = Math.min(times.length, 2);
154
+ }
155
+ examined.set(id, stream.length);
156
+ }
157
+ if (ids.every(id => after.get(id).length === 2 && sent >= after.get(id)[1])) break;
158
+ }
159
+ const index = new PacketIndex();
160
+ let startCovered = true;
161
+ for (const track of requested) {
162
+ const id = track.trackNumber, stream = frames.get(id);
163
+ declareTrackOf(index, track);
164
+ const last = reachedEnd ? Infinity : after.get(id)[0];
165
+ let kept = 0;
166
+ for (let position = 0; position < stream.length; position++) if (stream.ptsAt(position) <= last) kept = position + 1;
167
+ setDurations(stream, kept, track, info.durationSeconds, undefined);
168
+ stream.length = kept;
169
+ startCovered &&= coversStart(stream, track, interval.from);
170
+ index.sharePackets(id, stream);
171
+ if (reachedEnd) index.complete(id);
172
+ else index.coverThrough(id, last);
173
+ }
174
+ return { index, startCovered };
175
+ }
176
+
177
+ /** How far before an interval a window holds sound: its decoder's preroll and an MP3 reservoir lie well within it. */
178
+ export const WINDOW_LEAD_SECONDS = 1;
179
+ /** How far before its mark a window may begin: the search for its first packet stops within it. */
180
+ export const WINDOW_SEARCH_SECONDS = 2;
181
+ /** How many packets one read of a window takes. */
182
+ const WINDOW_PORTION_PACKETS = 64;
183
+
184
+ function coversStart(stream, track, from) {
185
+ if (!stream.length) return false;
186
+ if (track.type === "audio") return stream.ptsAt(0) <= from - WINDOW_LEAD_SECONDS;
187
+ for (let position = 0; position < stream.length; position++) {
188
+ if (stream.keyframeAt(position) && stream.ptsAt(position) <= from) return true;
189
+ }
190
+ return false;
191
+ }
192
+
193
+ function declareTrackOf(index, track) {
194
+ index.declareTrack(track.trackNumber, { type: track.type, codecId: track.codecId, codecRanges: track.codecRanges ?? [],
195
+ prerollSeconds: track.seekPrerollSeconds ?? 0 });
196
+ }
197
+
198
+ /** The first `count` records' durations, from the next presentation time where the format states none. */
199
+ function setDurations(stream, count, track, endSeconds, packetMemory) {
200
+ const following = (track.defaultDurationSeconds === undefined || track.defaultDurationSeconds === null) && stream.some(frame => frame.duration === undefined)
201
+ ? followingPacketTime(stream, endSeconds, packetMemory) : null;
202
+ try { for (let position = 0; position < count; position++) {
203
+ const stated = stream.durationAt(position), pts = stream.ptsAt(position);
204
+ const duration = !Number.isNaN(stated) ? stated : track.defaultDurationSeconds ?? following(pts) - pts;
205
+ if (!(duration > 0)) throw new Error("ASF frame duration cannot be determined.");
206
+ if (duration !== stated) stream.setDuration(position, duration);
207
+ } } finally { following?.dispose(); }
208
+ }
209
+
210
+ async function readPacketHeader(fields, packetLength) {
211
+ let flags = await fields.uint(1);
212
+ if (flags & 0x80) {
213
+ if (flags & 0x60) throw new Error("ASF error-correction length type is unsupported.");
214
+ fields.skip(flags & 15);
215
+ flags = await fields.uint(1);
216
+ }
217
+ const properties = await fields.uint(1);
218
+ const length = await fields.sized(flags >> 5, packetLength);
219
+ await fields.sized(flags >> 1, 0);
220
+ const padding = await fields.sized(flags >> 3, 0);
221
+ const timestamp = await fields.uint(4);
222
+ await fields.uint(2);
223
+ return { flags, properties, length, padding, timestamp };
224
+ }
225
+
226
+ /**
227
+ * One data packet's payloads, recorded into `frames` and `assembling`.
228
+ * `started`, when given, holds the streams whose media objects a walk that
229
+ * began after the first packet has seen begin; a later fragment of an object
230
+ * that began before the walk is passed over until one begins.
231
+ *
232
+ * @returns {Promise<{ next: number, sent: number }>} Where the next packet
233
+ * begins, and when this one is sent in seconds on its payloads' clock.
234
+ */
235
+ async function readPacket({ readRange, at, end, info, declared, frames, assembling, started = null }) {
236
+ const fields = new Fields(readRange, at, end);
237
+ const { flags, properties, length, padding, timestamp } = await readPacketHeader(fields, info.packetLength);
238
+ if (!length || length > end - at || padding >= length) throw new Error("ASF packet length or padding is invalid.");
239
+ const limit = at + length - padding;
240
+ fields.limit = limit;
241
+ const payloadFlags = flags & 1 ? await fields.uint(1) : 0x81;
242
+ const payloadCount = flags & 1 ? payloadFlags & 63 : 1;
243
+ if (!payloadCount) throw new Error("ASF packet has no payloads.");
244
+ for (let payload = 0; payload < payloadCount; payload++) {
245
+ const stream = await fields.uint(1);
246
+ const id = stream & 127, keyframe = (stream & 128) !== 0;
247
+ if (!declared.has(id)) throw new Error("ASF payload names an undeclared stream.");
248
+ const number = await fields.sized(properties >> 4, 0);
249
+ const offset = await fields.sized(properties >> 2, 0);
250
+ const replicaLength = await fields.sized(properties, 0);
251
+ let objectSize = null, time = timestamp, delta = null;
252
+ if (replicaLength >= 8) {
253
+ objectSize = await fields.uint(4);
254
+ time = await fields.uint(4);
255
+ fields.skip(replicaLength - 8);
256
+ } else if (replicaLength === 1) {
257
+ time = offset;
258
+ delta = await fields.uint(1);
259
+ } else if (replicaLength !== 0) throw new Error("ASF replicated payload data is invalid.");
260
+ const payloadSize = flags & 1 ? await fields.sized(payloadFlags >> 6, 0) : limit - fields.at;
261
+ if (!payloadSize || payloadSize > limit - fields.at) throw new Error("ASF payload exceeds its packet.");
262
+ const payloadEnd = fields.at + payloadSize;
263
+ if (delta !== null) {
264
+ started?.add(id);
265
+ while (fields.at < payloadEnd) {
266
+ const frameSize = await fields.uint(1);
267
+ if (!frameSize || frameSize > payloadEnd - fields.at) throw new Error("ASF compressed frame exceeds its payload.");
268
+ frames.get(id).push({ pts: time / 1000 - info.prerollSeconds, ...(delta > 0 ? { duration: delta / 1000 } : {}),
269
+ ...(isMp3Codec(declared.get(id)?.codecId) ? { decodeFromIndex: frames.get(id).length, decodeDependencyUnknown: true } : {}),
270
+ keyframe, ranges: [[fields.at, fields.at + frameSize - 1]] });
271
+ fields.skip(frameSize);
272
+ time += delta;
273
+ }
274
+ } else {
275
+ let object = assembling.get(id);
276
+ if (offset !== 0 && !object && started && !started.has(id)) {
277
+ fields.skip(payloadSize);
278
+ continue;
279
+ }
280
+ if (offset === 0) {
281
+ if (object) throw new Error("ASF media object ended before all fragments arrived.");
282
+ if (!(objectSize > 0)) throw new Error("ASF media object size is absent.");
283
+ object = { number, size: objectSize, received: 0, pts: time / 1000 - info.prerollSeconds, keyframe, ranges: [] };
284
+ assembling.set(id, object);
285
+ started?.add(id);
286
+ }
287
+ if (!object || object.number !== number || object.received !== offset ||
288
+ (objectSize !== null && object.size !== objectSize) || offset + payloadSize > object.size) {
289
+ throw new Error("ASF media object fragments are incomplete or inconsistent.");
290
+ }
291
+ object.ranges.push([fields.at, payloadEnd - 1]);
292
+ object.received += payloadSize;
293
+ fields.skip(payloadSize);
294
+ if (object.received === object.size) {
295
+ frames.get(id).push({ pts: object.pts, keyframe: object.keyframe, ranges: object.ranges,
296
+ ...(isMp3Codec(declared.get(id)?.codecId) ? { decodeFromIndex: frames.get(id).length, decodeDependencyUnknown: true } : {}) });
297
+ assembling.delete(id);
298
+ }
299
+ }
300
+ }
301
+ if (fields.at !== limit) throw new Error("ASF payload count differs from its packet size.");
302
+ return { next: at + Math.max(length, info.minimumPacketLength ?? length), sent: timestamp / 1000 - info.prerollSeconds };
303
+ }
304
+
138
305
  class Fields {
139
306
  constructor(read, at, limit) { this.read = read; this.at = at; this.limit = limit; }
140
307
  skip(count) {