@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,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
|
-
/**
|
|
9
|
-
export async function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
123
|
-
|
|
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) {
|