@torrent-tv/proxy 2.90.0 → 2.92.0
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/bin/cli.js +5 -2
- package/package.json +18 -2
- package/routes/api/playback-plan/post.js +21 -4
- package/routes/api/sources/files/get.js +20 -28
- package/routes/api/sources/post.js +4 -2
- package/routes/api/sources/stats/get.js +4 -11
- package/routes/api/sources/viewer/post.js +16 -0
- package/routes/api/sources/warm/post.js +6 -215
- package/routes/api/subtitles/get.js +22 -125
- package/routes/api/subtitles/post.js +26 -0
- package/routes/api/transcode-sessions/post.js +24 -2
- package/routes/media/get.js +80 -0
- package/routes/stream/get.js +26 -94
- package/routes/transcode/audio-warm/get.js +9 -21
- package/routes/transcode/variant-warm/get.js +8 -14
- package/server.js +410 -46
- package/services/encode/AdmittedInput.js +136 -0
- package/services/encode/CushionReport.js +1 -76
- package/services/encode/EncodeInputs.js +142 -0
- package/services/encode/EncodeOrchestrator.js +4 -1
- package/services/encode/EncodeRun.js +13 -4
- package/services/encode/EncodeRuns.js +132 -52
- package/services/encode/InputFailures.js +37 -0
- package/services/encode/MatroskaInput.js +158 -0
- package/services/encode/OutputOpening.js +16 -64
- package/services/encode/Renditions.js +1 -17
- package/services/encode/admitted-command.js +60 -0
- package/services/encode/annex-b-input.js +37 -0
- package/services/encode/bit-payload.js +29 -0
- package/services/encode/output/OutputCatalog.js +1 -1
- package/services/encode/output/Timeline.js +11 -0
- package/services/encode/pcm-normalization.js +30 -0
- package/services/encode/piece-completeness.js +54 -1
- package/services/encode/quality/HostLoad.js +19 -73
- package/services/encode/quality/QualityOffer.js +1 -1
- package/services/encode/run-command.js +1 -589
- package/services/encode/segment-formats/mp4-boxes.js +3 -1
- package/services/encode/segment-formats/presentation-segment.js +114 -0
- package/services/media/ContainerOrchestrator.js +118 -48
- package/services/media/KeyframeTables.js +16 -115
- package/services/media/MediaReadRequests.js +120 -0
- package/services/media/ProbeRequests.js +135 -0
- package/services/media/SegmentInputs.js +48 -0
- package/services/media/SourceFile.js +6 -0
- package/services/media/SubtitleOrchestrator.js +190 -4
- package/services/media/await-plan.js +47 -0
- package/services/media/container/AsfContainer.js +217 -0
- package/services/media/container/AviContainer.js +343 -34
- package/services/media/container/CadencedPictureOrder.js +45 -0
- package/services/media/container/Container.js +7 -3
- package/services/media/container/ContainerFactory.js +10 -3
- package/services/media/container/FfprobeContainer.js +222 -0
- package/services/media/container/MatroskaContainer.js +72 -46
- package/services/media/container/Mp4Container.js +472 -139
- package/services/media/container/MpegPsContainer.js +281 -0
- package/services/media/container/MpegTsContainer.js +277 -0
- package/services/media/container/PacketIndex.js +270 -0
- package/services/media/container/PacketRecords.js +237 -0
- package/services/media/container/RetainedBytes.js +39 -0
- package/services/media/container/RetainedReads.js +51 -0
- package/services/media/container/ac3-frame.js +46 -0
- package/services/media/container/asf-packets.js +153 -0
- package/services/media/container/avc-picture-order.js +50 -0
- package/services/media/container/avi-open-dml.js +47 -0
- package/services/media/container/dts-frame.js +51 -0
- package/services/media/container/ebml-stream.js +12 -1
- package/services/media/container/ffprobe-record.js +45 -0
- package/services/media/container/flv-packet-ranges.js +27 -0
- package/services/media/container/following-packet-time.js +37 -0
- package/services/media/container/h264-configuration.js +183 -0
- package/services/media/container/h264-elementary-index.js +245 -0
- package/services/media/container/hevc-configuration.js +165 -0
- package/services/media/container/hevc-elementary-index.js +132 -0
- package/services/media/container/hevc-picture-order.js +66 -0
- package/services/media/container/latm-frame.js +99 -0
- package/services/media/container/matroska-packets.js +207 -0
- package/services/media/container/memory-unavailable.js +8 -0
- package/services/media/container/mp3-packet-dependencies.js +41 -0
- package/services/media/container/mp4-fragments.js +148 -0
- package/services/media/container/mpeg-audio-frame.js +19 -0
- package/services/media/container/mpeg-audio-reservoir.js +27 -0
- package/services/media/container/mpeg-elementary-index.js +196 -0
- package/services/media/container/mpeg-sequence-reader.js +69 -0
- package/services/media/container/mpeg-ts-packets.js +93 -0
- package/services/media/container/mpeg-video-configuration.js +50 -0
- package/services/media/container/mpeg4-picture-timing.js +108 -0
- package/services/media/container/pes-header.js +34 -0
- package/services/media/playback-declarations.js +57 -0
- package/services/media/playback-planner.js +72 -230
- package/services/media/probe-packets.js +61 -0
- package/services/media/read-playback-declarations.js +18 -0
- package/services/media/tracks/AudioTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +4 -1
- package/services/server/OutputLifecycle.js +2 -0
- package/services/server/SegmentServing.js +40 -33
- package/services/server/ViewerRequests.js +22 -41
- package/services/server/controllers/PlaybackController.js +4 -0
- package/services/server/controllers/SubtitleController.js +8 -29
- package/services/server/transcode-session-files.js +58 -99
- package/services/server/wire-outputs.js +58 -15
- package/services/storage/IndexMemory.js +85 -0
- package/services/storage/MachineBudget.js +27 -3
- package/services/storage/allowance.js +8 -1
- package/services/storage/piece-store/piece-disk-store.js +18 -3
- package/services/storage/piece-store/piece-lru.js +8 -4
- package/services/storage/piece-store/shared-piece-store.js +26 -11
- package/services/storage/segment-store/SegmentStore.js +49 -18
- package/services/storage/wire.js +7 -0
- package/services/torrent/Contents.js +52 -20
- package/services/torrent/demand/DemandRegister.js +13 -4
- package/services/torrent/demand/Window.js +13 -2
- package/services/torrent/demand-progress.js +25 -0
- package/services/torrent/download/DeadlineScheduler.js +167 -0
- package/services/torrent/download/FutureDownload.js +166 -0
- package/services/torrent/download/SwarmSelection.js +30 -59
- package/services/torrent/download/registry.js +45 -5
- package/services/torrent/download/withdraw-claim.js +2 -0
- package/services/torrent/torrent-pool.js +39 -600
- package/services/torrent/worker/channel.js +8 -1
- package/services/torrent/worker/client.js +74 -72
- package/services/torrent/worker/files-in-use.js +9 -0
- package/services/torrent/worker/handle-state.js +12 -0
- package/services/torrent/worker/held-bytes.js +82 -99
- package/services/torrent/worker/held-file-bytes.js +17 -0
- package/services/torrent/worker/piece-reader.js +67 -683
- package/services/torrent/worker/pool-adapter.js +37 -123
- package/services/torrent/worker/protocol.js +6 -17
- package/services/torrent/worker/whole-file-stats.js +2 -0
- package/services/torrent/worker/worker.js +86 -156
- package/services/transport/data-channel-handler.js +56 -13
- package/services/transport/tunnel-client.js +111 -20
- package/services/viewer/DownloadMaps.js +168 -0
- package/services/viewer/NativeSourceMap.js +33 -0
- package/services/viewer/PriorityMap.js +49 -40
- package/services/viewer/PriorityOrchestrator.js +42 -13
- package/services/viewer/SourcePreparation.js +278 -0
- package/services/viewer/Viewer.js +11 -0
- package/services/viewer/Viewers.js +109 -2
- package/services/viewer/playback-readiness.js +50 -63
- package/services/viewer/segment-demands.js +37 -0
- package/services/viewer/urgent-output-ready.js +26 -0
- package/services/encode/run-inputs.js +0 -53
- package/services/media/keyframe-probe.js +0 -157
- package/services/media/media-info-probe.js +0 -93
- package/services/torrent/worker/background-fill.js +0 -213
- package/services/torrent/worker/resume-warm.js +0 -107
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { PacketRecords } from "./PacketRecords.js";
|
|
2
|
+
import { IndexMemoryUnavailable } from "./memory-unavailable.js";
|
|
3
|
+
import { isMp3Codec, mp3PacketFacts } from "./mp3-packet-dependencies.js";
|
|
4
|
+
|
|
5
|
+
/** Exact packet addresses and decode order, owned by one immutable source file. */
|
|
6
|
+
export class PacketIndex {
|
|
7
|
+
#tracks = new Map();
|
|
8
|
+
#packetMemory;
|
|
9
|
+
#deferMemory;
|
|
10
|
+
#pending = [];
|
|
11
|
+
#pendingAt = 0;
|
|
12
|
+
|
|
13
|
+
constructor({ packetMemory, deferMemory = false } = {}) {
|
|
14
|
+
this.#packetMemory = packetMemory;
|
|
15
|
+
this.#deferMemory = deferMemory;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declareTrack(id, { type, codecId = "", codecRanges = [], prerollSeconds = 0, reorderDepth = 0 }) {
|
|
19
|
+
if (this.#tracks.has(id)) throw new Error(`Packet track ${id} was already declared.`);
|
|
20
|
+
if (!["video", "audio", "subtitle"].includes(type) ||
|
|
21
|
+
!Number.isFinite(prerollSeconds) || prerollSeconds < 0 ||
|
|
22
|
+
!Number.isSafeInteger(reorderDepth) || reorderDepth < 0) {
|
|
23
|
+
throw new TypeError("Invalid packet track parameters.");
|
|
24
|
+
}
|
|
25
|
+
const prerollFrames = isMp3Codec(codecId) || ["aac", "aac_latm", "mp4a"].includes(codecId) || /^A_AAC(?:\/|$)/.test(codecId) ? 1 : 0;
|
|
26
|
+
this.#tracks.set(id, { type, codecId, codecRanges: normalizeRanges(codecRanges), prerollSeconds, prerollFrames, reorderDepth, packets: new PacketRecords(this.#packetMemory), coveredThrough: 0, complete: false });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
append(id, packet) {
|
|
30
|
+
const track = this.#track(id);
|
|
31
|
+
if (track.complete) throw new Error("Cannot append to a completed packet index.");
|
|
32
|
+
const ranges = validatePacket(packet);
|
|
33
|
+
if (packet.decodeFromIndex !== undefined && (!Number.isSafeInteger(packet.decodeFromIndex) || packet.decodeFromIndex < 0 ||
|
|
34
|
+
packet.decodeFromIndex > track.packets.length + this.#pending.slice(this.#pendingAt).filter(one => one?.id === id).length)) {
|
|
35
|
+
throw new TypeError("A packet decoder dependency must name an existing or current packet.");
|
|
36
|
+
}
|
|
37
|
+
const record = { pts: packet.pts, duration: packet.duration, keyframe: packet.keyframe === true, ranges,
|
|
38
|
+
...(packet.expectedHash === undefined ? {} : { expectedHash: packet.expectedHash }),
|
|
39
|
+
...(packet.discardPaddingSeconds === undefined ? {} : { discardPaddingSeconds: packet.discardPaddingSeconds }),
|
|
40
|
+
...(packet.bitOffset === undefined ? {} : { bitOffset: packet.bitOffset, bitLength: packet.bitLength }),
|
|
41
|
+
...(packet.dts === undefined ? {} : { dts: packet.dts }) };
|
|
42
|
+
if (packet.decodeFromIndex !== undefined) record.decodeFromIndex = packet.decodeFromIndex;
|
|
43
|
+
if (packet.decodeDependencyUnknown) record.decodeDependencyUnknown = true;
|
|
44
|
+
if (track.type === "audio" && isMp3Codec(track.codecId) && record.decodeFromIndex === undefined) {
|
|
45
|
+
record.decodeFromIndex = track.packets.length + this.#pending.slice(this.#pendingAt).filter(one => one?.id === id).length;
|
|
46
|
+
record.decodeDependencyUnknown = true;
|
|
47
|
+
}
|
|
48
|
+
if (this.#pending.length) { this.#pending.push({ id, record }); return; }
|
|
49
|
+
try { track.packets.push(record); }
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!this.#deferMemory || !(error instanceof IndexMemoryUnavailable)) throw error;
|
|
52
|
+
this.#pending.push({ id, record });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Drain facts from the last transport read before accepting another read. */
|
|
57
|
+
flushPending() {
|
|
58
|
+
if (!this.#pending.length) return;
|
|
59
|
+
while (this.#pendingAt < this.#pending.length) {
|
|
60
|
+
const { id, record } = this.#pending[this.#pendingAt];
|
|
61
|
+
this.#track(id).packets.push(record);
|
|
62
|
+
this.#pending[this.#pendingAt++] = null;
|
|
63
|
+
}
|
|
64
|
+
this.#pending = [];
|
|
65
|
+
this.#pendingAt = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The progressive parser and its interval views share one packet allocation. */
|
|
69
|
+
async prepareAudioDependencies(interval, readRange) {
|
|
70
|
+
this.flushPending();
|
|
71
|
+
for (const [id, track] of this.#tracks) {
|
|
72
|
+
if (track.type !== "audio" || !isMp3Codec(track.codecId) ||
|
|
73
|
+
(interval?.trackIds && !interval.trackIds.includes(id)) || interval?.modes?.[id] === "copy") continue;
|
|
74
|
+
const records = track.packets;
|
|
75
|
+
for (let position = 0; position < records.length; position++) {
|
|
76
|
+
const packet = records.at(position);
|
|
77
|
+
if (!packet.decodeDependencyUnknown || (interval &&
|
|
78
|
+
(packet.pts >= interval.to || (packet.pts < interval.from && packet.pts + packet.duration <= interval.from &&
|
|
79
|
+
(position + track.prerollFrames >= records.length || records.ptsAt(position + track.prerollFrames) + records.durationAt(position + track.prerollFrames) <= interval.from))))) continue;
|
|
80
|
+
const facts = await mp3PacketFacts(packet, readRange);
|
|
81
|
+
let required = facts.required, first = position;
|
|
82
|
+
while (required > 0 && first > 0) {
|
|
83
|
+
first--;
|
|
84
|
+
required -= (await mp3PacketFacts(records.at(first), readRange)).capacity;
|
|
85
|
+
}
|
|
86
|
+
if (required > 0) throw new Error("MP3 bit reservoir refers to bytes before the indexed source.");
|
|
87
|
+
records.setDecodeFromIndex(position, first);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The progressive parser and its interval views share one packet allocation. */
|
|
93
|
+
sharePackets(id, records) {
|
|
94
|
+
const track = this.#track(id);
|
|
95
|
+
if (track.complete || track.packets.length || !(records instanceof PacketRecords)) {
|
|
96
|
+
throw new TypeError("Only an empty unfinished track may adopt packet records.");
|
|
97
|
+
}
|
|
98
|
+
for (const packet of records) validatePacket(packet);
|
|
99
|
+
track.packets.dispose();
|
|
100
|
+
track.packets = records;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
dispose() {
|
|
104
|
+
for (const track of this.#tracks.values()) track.packets.dispose();
|
|
105
|
+
this.#tracks.clear();
|
|
106
|
+
this.#pending = [];
|
|
107
|
+
this.#pendingAt = 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
reservePackets(id, count) {
|
|
111
|
+
const track = this.#track(id);
|
|
112
|
+
track.packets.reserveCapacity(count, isMp3Codec(track.codecId) ? 76 : 72);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
complete(id) {
|
|
116
|
+
const track = this.#track(id);
|
|
117
|
+
track.packets.releaseUnusedCapacity();
|
|
118
|
+
track.complete = true;
|
|
119
|
+
}
|
|
120
|
+
extendLastPresentation(id, seconds) {
|
|
121
|
+
const track = this.#track(id);
|
|
122
|
+
if (track.complete || track.type !== "video" || !Number.isFinite(seconds) || seconds <= 0) throw new TypeError("Only an unfinished video packet may extend its presentation.");
|
|
123
|
+
track.packets.extendLastPresentation(seconds);
|
|
124
|
+
}
|
|
125
|
+
refuse(id, reason) {
|
|
126
|
+
if (typeof reason !== "string" || !reason) throw new TypeError("A packet refusal requires a reason.");
|
|
127
|
+
const track = this.#track(id);
|
|
128
|
+
track.refusal = reason;
|
|
129
|
+
track.complete = true;
|
|
130
|
+
}
|
|
131
|
+
isComplete() { return [...this.#tracks.values()].every(track => track.complete); }
|
|
132
|
+
|
|
133
|
+
allocatedBytes() { return [...this.#tracks.values()].reduce((bytes, track) => bytes + track.packets.allocatedBytes, 0); }
|
|
134
|
+
|
|
135
|
+
boundsOf(id) {
|
|
136
|
+
this.flushPending();
|
|
137
|
+
const track = this.#track(id);
|
|
138
|
+
if (track.bounds) return track.bounds;
|
|
139
|
+
const packets = track.packets;
|
|
140
|
+
if (!packets.length) return null;
|
|
141
|
+
let start = Infinity, end = -Infinity;
|
|
142
|
+
for (let index = 0; index < packets.length; index++) {
|
|
143
|
+
const pts = packets.ptsAt(index);
|
|
144
|
+
start = Math.min(start, pts);
|
|
145
|
+
end = Math.max(end, pts + packets.durationAt(index));
|
|
146
|
+
}
|
|
147
|
+
const bounds = { start, end };
|
|
148
|
+
if (track.complete) track.bounds = bounds;
|
|
149
|
+
return bounds;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
keyframesOf(id) {
|
|
153
|
+
this.flushPending();
|
|
154
|
+
const packets = this.#track(id).packets, times = [];
|
|
155
|
+
for (let index = 0; index < packets.length; index++) if (packets.keyframeAt(index)) times.push(packets.ptsAt(index));
|
|
156
|
+
return times.sort((a, b) => a - b);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
coverThrough(id, seconds) {
|
|
160
|
+
const track = this.#track(id);
|
|
161
|
+
if (!Number.isFinite(seconds) || seconds < track.coveredThrough) throw new TypeError("Index coverage must advance.");
|
|
162
|
+
track.coveredThrough = seconds;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The same calculation is used to order source bytes and admit an encode. */
|
|
166
|
+
inputFor({ trackId, from, to, mode = "transcode" }) {
|
|
167
|
+
this.flushPending();
|
|
168
|
+
if (!["copy", "transcode"].includes(mode)) throw new TypeError("Unknown segment input mode.");
|
|
169
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || from < 0 || to <= from) {
|
|
170
|
+
throw new TypeError("A segment requires a finite nonempty presentation interval.");
|
|
171
|
+
}
|
|
172
|
+
const track = this.#track(trackId);
|
|
173
|
+
if (track.refusal) return { kind: "terminal", reason: track.refusal, trackId };
|
|
174
|
+
const packets = track.packets;
|
|
175
|
+
const startAt = from - (track.type === "audio" && mode === "transcode" ? track.prerollSeconds : 0);
|
|
176
|
+
const selected = [];
|
|
177
|
+
for (let index = 0; index < packets.length; index++) {
|
|
178
|
+
const pts = packets.ptsAt(index);
|
|
179
|
+
if (pts < to && (pts >= startAt ||
|
|
180
|
+
((mode === "transcode" || track.type === "audio") && pts + packets.durationAt(index) > startAt))) selected.push(index);
|
|
181
|
+
}
|
|
182
|
+
if (selected.length === 0) {
|
|
183
|
+
if (track.type === "subtitle" && (track.complete || track.coveredThrough >= to)) {
|
|
184
|
+
return { kind: "result", trackId, ranges: [], packets: [], from, to, decodeFrom: from };
|
|
185
|
+
}
|
|
186
|
+
return track.complete
|
|
187
|
+
? { kind: "terminal", reason: "track-has-no-packets-in-interval" }
|
|
188
|
+
: { kind: "needs-index", trackId, from: startAt, to };
|
|
189
|
+
}
|
|
190
|
+
let first = selected[0];
|
|
191
|
+
let last = selected[selected.length - 1];
|
|
192
|
+
if (track.type === "audio" && mode === "transcode") {
|
|
193
|
+
const firstPlayed = selected.find(index => packets.ptsAt(index) + packets.durationAt(index) > from);
|
|
194
|
+
if (firstPlayed !== undefined) first = Math.min(first, Math.max(0, firstPlayed - track.prerollFrames));
|
|
195
|
+
for (let index = first; index <= last; index++) first = Math.min(first, packets.at(index).decodeFromIndex ?? index);
|
|
196
|
+
}
|
|
197
|
+
if (track.type === "video") {
|
|
198
|
+
while (first >= 0 && (!packets.keyframeAt(first) || packets.ptsAt(first) > from)) first--;
|
|
199
|
+
if (first < 0) return track.complete
|
|
200
|
+
? { kind: "terminal", reason: "video-interval-has-no-decode-start" }
|
|
201
|
+
: { kind: "needs-index", trackId, from: startAt, to };
|
|
202
|
+
let endsAtKeyframe = false;
|
|
203
|
+
if (mode === "copy") for (let index = 0; index < packets.length; index++) if (packets.ptsAt(index) === to && packets.keyframeAt(index)) { endsAtKeyframe = true; break; }
|
|
204
|
+
if (mode === "copy" && (packets.ptsAt(first) !== from ||
|
|
205
|
+
(!endsAtKeyframe && (!track.complete || to < this.boundsOf(trackId).end)))) {
|
|
206
|
+
return { kind: "terminal", reason: "video-copy-cut-is-not-a-keyframe" };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Coverage is established by the parser after its codec-required lookahead;
|
|
210
|
+
// packets outside the interval are inspected but are not appended to input.
|
|
211
|
+
if (!track.complete && track.coveredThrough < to) {
|
|
212
|
+
return { kind: "needs-index", trackId, from: startAt, to };
|
|
213
|
+
}
|
|
214
|
+
last = Math.min(last, packets.length - 1);
|
|
215
|
+
const input = packets.slice(first, last + 1);
|
|
216
|
+
return {
|
|
217
|
+
kind: "result",
|
|
218
|
+
trackId,
|
|
219
|
+
ranges: normalizeRanges([...track.codecRanges, ...input.flatMap(packet => packet.ranges)]),
|
|
220
|
+
decodeFrom: packets.ptsAt(first),
|
|
221
|
+
from,
|
|
222
|
+
to,
|
|
223
|
+
...(track.complete && to >= this.boundsOf(trackId).end ? { sourceEndSeconds: this.boundsOf(trackId).end } : {}),
|
|
224
|
+
packets: input
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#track(id) {
|
|
229
|
+
const track = this.#tracks.get(id);
|
|
230
|
+
if (!track) throw new Error(`Packet track ${id} is not declared.`);
|
|
231
|
+
return track;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function validatePacket(packet) {
|
|
236
|
+
if (!Number.isFinite(packet.pts) || !Number.isFinite(packet.duration) || packet.duration < 0) {
|
|
237
|
+
throw new TypeError("Packet presentation time and duration are required.");
|
|
238
|
+
}
|
|
239
|
+
const ranges = normalizeRanges(packet.ranges);
|
|
240
|
+
if (ranges.length === 0) throw new TypeError("Packet byte addresses are required.");
|
|
241
|
+
if (packet.dts !== undefined && !Number.isFinite(packet.dts)) throw new TypeError("Packet decode time must be finite.");
|
|
242
|
+
if (packet.discardPaddingSeconds !== undefined && !Number.isSafeInteger(Math.round(packet.discardPaddingSeconds * 1e9))) {
|
|
243
|
+
throw new TypeError("Packet discard padding must have an exact finite duration.");
|
|
244
|
+
}
|
|
245
|
+
if (packet.expectedHash !== undefined && !/^[a-f0-9]{64}$/.test(packet.expectedHash)) throw new TypeError("Packet SHA-256 must be hexadecimal.");
|
|
246
|
+
if ((packet.bitOffset === undefined) !== (packet.bitLength === undefined)) throw new TypeError("Packet bit addresses need both offset and length.");
|
|
247
|
+
if (packet.bitOffset !== undefined && (!Number.isSafeInteger(packet.bitOffset) || packet.bitOffset < 0 ||
|
|
248
|
+
!Number.isSafeInteger(packet.bitLength) || packet.bitLength <= 0 || packet.bitLength % 8 !== 0 ||
|
|
249
|
+
packet.bitOffset + packet.bitLength > ranges.reduce((sum, [start, end]) => sum + end - start + 1, 0) * 8)) {
|
|
250
|
+
throw new TypeError("Packet payload bit addresses are invalid.");
|
|
251
|
+
}
|
|
252
|
+
return ranges;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Inclusive byte ranges, merged without filling gaps between separate packets. */
|
|
256
|
+
function normalizeRanges(ranges) {
|
|
257
|
+
const ordered = ranges.map(([start, end]) => {
|
|
258
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) {
|
|
259
|
+
throw new TypeError("Invalid packet byte range.");
|
|
260
|
+
}
|
|
261
|
+
return [start, end];
|
|
262
|
+
}).sort((left, right) => left[0] - right[0]);
|
|
263
|
+
const result = [];
|
|
264
|
+
for (const range of ordered) {
|
|
265
|
+
const previous = result[result.length - 1];
|
|
266
|
+
if (previous && range[0] <= previous[1] + 1) previous[1] = Math.max(previous[1], range[1]);
|
|
267
|
+
else result.push(range);
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Fixed-width packet facts and exact range addresses, outside the object heap.
|
|
2
|
+
// Allocation size is a storage block size, not an admission allowance.
|
|
3
|
+
import { IndexMemoryUnavailable } from "./memory-unavailable.js";
|
|
4
|
+
|
|
5
|
+
const BLOCK_BYTES = 65536;
|
|
6
|
+
const ADDRESS_BYTES = 8;
|
|
7
|
+
const HEADER_BYTES = 56;
|
|
8
|
+
|
|
9
|
+
export class PacketRecords {
|
|
10
|
+
#blocks = [];
|
|
11
|
+
#addresses = [];
|
|
12
|
+
#used = 0;
|
|
13
|
+
#length = 0;
|
|
14
|
+
#bytes = 0;
|
|
15
|
+
#reserve;
|
|
16
|
+
#release;
|
|
17
|
+
#onDispose;
|
|
18
|
+
#disposed = false;
|
|
19
|
+
#prepaid = 0;
|
|
20
|
+
|
|
21
|
+
constructor(memory = {}) {
|
|
22
|
+
const allocation = memory.forRecord?.(this) ?? memory;
|
|
23
|
+
this.#reserve = allocation.reserve ?? (() => true);
|
|
24
|
+
this.#release = allocation.release ?? (() => {});
|
|
25
|
+
this.#onDispose = allocation.dispose;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dispose() {
|
|
29
|
+
if (this.#disposed) return;
|
|
30
|
+
this.length = 0;
|
|
31
|
+
this.releaseUnusedCapacity();
|
|
32
|
+
this.#disposed = true;
|
|
33
|
+
this.#onDispose?.();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get length() { return this.#length; }
|
|
37
|
+
get allocatedBytes() { return this.#bytes; }
|
|
38
|
+
|
|
39
|
+
/** Admit a declared table's complete allocation before reconstructing it. */
|
|
40
|
+
reserveCapacity(count, recordBytes) {
|
|
41
|
+
if (this.#disposed || this.#length || this.#prepaid || !Number.isSafeInteger(count) || count < 0 ||
|
|
42
|
+
!Number.isSafeInteger(recordBytes) || recordBytes < HEADER_BYTES || recordBytes > BLOCK_BYTES) {
|
|
43
|
+
throw new TypeError("Invalid declared packet capacity.");
|
|
44
|
+
}
|
|
45
|
+
const bytes = Math.ceil(count / Math.floor(BLOCK_BYTES / recordBytes)) * BLOCK_BYTES +
|
|
46
|
+
Math.ceil(count / (BLOCK_BYTES / ADDRESS_BYTES)) * BLOCK_BYTES;
|
|
47
|
+
if (!Number.isSafeInteger(bytes)) throw new TypeError("Declared packet capacity exceeds its integer range.");
|
|
48
|
+
if (bytes && !this.#reserve(bytes)) throw new IndexMemoryUnavailable(bytes);
|
|
49
|
+
this.#prepaid = bytes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
releaseUnusedCapacity() {
|
|
53
|
+
if (this.#prepaid) this.#release(this.#prepaid);
|
|
54
|
+
this.#prepaid = 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
set length(value) {
|
|
58
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > this.#length) {
|
|
59
|
+
throw new TypeError("Packet records may only be truncated.");
|
|
60
|
+
}
|
|
61
|
+
const previousBytes = this.#bytes;
|
|
62
|
+
if (value === 0) {
|
|
63
|
+
this.#blocks = [];
|
|
64
|
+
this.#addresses = [];
|
|
65
|
+
this.#used = 0;
|
|
66
|
+
this.#bytes = 0;
|
|
67
|
+
} else if (value < this.#length) {
|
|
68
|
+
const addressAt = (value - 1) * ADDRESS_BYTES;
|
|
69
|
+
const address = this.#addresses[Math.floor(addressAt / BLOCK_BYTES)];
|
|
70
|
+
const blockIndex = address.readUInt32LE(addressAt % BLOCK_BYTES);
|
|
71
|
+
const at = address.readUInt32LE(addressAt % BLOCK_BYTES + 4);
|
|
72
|
+
const block = this.#blocks[blockIndex];
|
|
73
|
+
this.#blocks.length = blockIndex + 1;
|
|
74
|
+
this.#addresses.length = Math.ceil(value * ADDRESS_BYTES / BLOCK_BYTES);
|
|
75
|
+
const flags = block.readUInt32LE(at + 48);
|
|
76
|
+
this.#used = at + HEADER_BYTES + block.readUInt32LE(at + 52) * 16 + ((flags & 2) ? 32 : 0) + ((flags & 8) ? 4 : 0) + ((flags & 16) ? 4 : 0);
|
|
77
|
+
this.#bytes = [...this.#blocks, ...this.#addresses].reduce((sum, buffer) => sum + buffer.length, 0);
|
|
78
|
+
}
|
|
79
|
+
this.#length = value;
|
|
80
|
+
if (previousBytes !== this.#bytes) this.#release(previousBytes - this.#bytes);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
push(packet) {
|
|
84
|
+
if (this.#disposed) throw new Error("Packet records were released.");
|
|
85
|
+
if (!Number.isFinite(packet.pts) || !Array.isArray(packet.ranges) ||
|
|
86
|
+
packet.ranges.some(range => !Array.isArray(range) || range.length !== 2 ||
|
|
87
|
+
!Number.isSafeInteger(range[0]) || !Number.isSafeInteger(range[1]) || range[0] < 0 || range[1] < range[0]) ||
|
|
88
|
+
[packet.duration, packet.dts, packet.discardPaddingSeconds, packet.bitOffset, packet.bitLength]
|
|
89
|
+
.some(value => value != null && !Number.isFinite(value)) ||
|
|
90
|
+
(packet.expectedHash !== undefined && !/^[\da-f]{64}$/i.test(packet.expectedHash)) ||
|
|
91
|
+
[packet.streamId, packet.decodeFromIndex].some(value => value !== undefined && (!Number.isInteger(value) || value < 0 || value > 0xffffffff))) {
|
|
92
|
+
throw new TypeError("Invalid binary packet facts.");
|
|
93
|
+
}
|
|
94
|
+
const bytes = HEADER_BYTES + packet.ranges.length * 16 + (packet.expectedHash ? 32 : 0) + (packet.streamId !== undefined ? 4 : 0) + (packet.decodeFromIndex !== undefined ? 4 : 0);
|
|
95
|
+
let block = this.#blocks.at(-1);
|
|
96
|
+
const newBlock = !block || block.length - this.#used < bytes;
|
|
97
|
+
const perAddressBlock = BLOCK_BYTES / ADDRESS_BYTES;
|
|
98
|
+
const addressBlockIndex = Math.floor(this.#length / perAddressBlock);
|
|
99
|
+
const newAddresses = !this.#addresses[addressBlockIndex];
|
|
100
|
+
const growth = (newBlock ? Math.max(BLOCK_BYTES, bytes) : 0) + (newAddresses ? BLOCK_BYTES : 0);
|
|
101
|
+
const prepaid = Math.min(growth, this.#prepaid);
|
|
102
|
+
if (growth > prepaid && !this.#reserve(growth - prepaid)) throw new IndexMemoryUnavailable(growth - prepaid);
|
|
103
|
+
this.#prepaid -= prepaid;
|
|
104
|
+
let payloadAllocation, addressAllocation;
|
|
105
|
+
try {
|
|
106
|
+
if (newBlock) payloadAllocation = Buffer.alloc(Math.max(BLOCK_BYTES, bytes));
|
|
107
|
+
if (newAddresses) addressAllocation = Buffer.alloc(BLOCK_BYTES);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (growth) this.#release(growth);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
if (newBlock) {
|
|
113
|
+
block = payloadAllocation;
|
|
114
|
+
this.#blocks.push(block);
|
|
115
|
+
this.#bytes += block.length;
|
|
116
|
+
this.#used = 0;
|
|
117
|
+
}
|
|
118
|
+
if (newAddresses) {
|
|
119
|
+
this.#addresses.push(addressAllocation);
|
|
120
|
+
this.#bytes += BLOCK_BYTES;
|
|
121
|
+
}
|
|
122
|
+
const address = this.#addresses[addressBlockIndex];
|
|
123
|
+
const addressAt = (this.#length % perAddressBlock) * ADDRESS_BYTES;
|
|
124
|
+
address.writeUInt32LE(this.#blocks.length - 1, addressAt);
|
|
125
|
+
address.writeUInt32LE(this.#used, addressAt + 4);
|
|
126
|
+
const at = this.#used;
|
|
127
|
+
const numbers = [packet.pts, packet.duration, packet.dts, packet.discardPaddingSeconds,
|
|
128
|
+
packet.bitOffset, packet.bitLength];
|
|
129
|
+
numbers.forEach((value, index) => block.writeDoubleLE(value ?? NaN, at + index * 8));
|
|
130
|
+
block.writeUInt32LE((packet.keyframe ? 1 : 0) | (packet.expectedHash ? 2 : 0) | (packet.streamId !== undefined ? 8 : 0) | (packet.decodeFromIndex !== undefined ? 16 : 0) | (packet.decodeFromIndex !== undefined && !packet.decodeDependencyUnknown ? 32 : 0), at + 48);
|
|
131
|
+
block.writeUInt32LE(packet.ranges.length, at + 52);
|
|
132
|
+
packet.ranges.forEach(([start, end], index) => {
|
|
133
|
+
block.writeDoubleLE(start, at + HEADER_BYTES + index * 16);
|
|
134
|
+
block.writeDoubleLE(end, at + HEADER_BYTES + index * 16 + 8);
|
|
135
|
+
});
|
|
136
|
+
if (packet.expectedHash) block.write(packet.expectedHash, at + HEADER_BYTES + packet.ranges.length * 16, 32, "hex");
|
|
137
|
+
if (packet.streamId !== undefined) block.writeUInt32LE(packet.streamId, at + bytes - 4 - (packet.decodeFromIndex !== undefined ? 4 : 0));
|
|
138
|
+
if (packet.decodeFromIndex !== undefined) block.writeUInt32LE(packet.decodeFromIndex, at + bytes - 4);
|
|
139
|
+
this.#used += bytes;
|
|
140
|
+
return ++this.#length;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#address(index) {
|
|
144
|
+
const addressAt = index * ADDRESS_BYTES;
|
|
145
|
+
const address = this.#addresses[Math.floor(addressAt / BLOCK_BYTES)];
|
|
146
|
+
const offset = addressAt % BLOCK_BYTES;
|
|
147
|
+
return [this.#blocks[address.readUInt32LE(offset)], address.readUInt32LE(offset + 4)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ptsAt(index) {
|
|
151
|
+
const [block, at] = this.#address(index);
|
|
152
|
+
return block.readDoubleLE(at);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
durationAt(index) {
|
|
156
|
+
const [block, at] = this.#address(index);
|
|
157
|
+
return block.readDoubleLE(at + 8);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
keyframeAt(index) {
|
|
161
|
+
const [block, at] = this.#address(index);
|
|
162
|
+
return !!(block.readUInt32LE(at + 48) & 1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
derivedDurationAt(index) {
|
|
166
|
+
const [block, at] = this.#address(index);
|
|
167
|
+
return !!(block.readUInt32LE(at + 48) & 4);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
at(index) {
|
|
171
|
+
if (index < 0) index += this.#length;
|
|
172
|
+
if (index < 0 || index >= this.#length) return undefined;
|
|
173
|
+
const [block, at] = this.#address(index);
|
|
174
|
+
const flags = block.readUInt32LE(at + 48), count = block.readUInt32LE(at + 52);
|
|
175
|
+
const packet = { pts: block.readDoubleLE(at), keyframe: !!(flags & 1), ranges: [] };
|
|
176
|
+
const duration = block.readDoubleLE(at + 8);
|
|
177
|
+
if (!Number.isNaN(duration)) packet.duration = duration;
|
|
178
|
+
for (const [number, name] of ["dts", "discardPaddingSeconds", "bitOffset", "bitLength"].entries()) {
|
|
179
|
+
const value = block.readDoubleLE(at + 16 + number * 8);
|
|
180
|
+
if (!Number.isNaN(value)) packet[name] = value;
|
|
181
|
+
}
|
|
182
|
+
for (let range = 0; range < count; range++) packet.ranges.push([
|
|
183
|
+
block.readDoubleLE(at + HEADER_BYTES + range * 16), block.readDoubleLE(at + HEADER_BYTES + range * 16 + 8)
|
|
184
|
+
]);
|
|
185
|
+
if (flags & 2) packet.expectedHash = block.subarray(at + HEADER_BYTES + count * 16, at + HEADER_BYTES + count * 16 + 32).toString("hex");
|
|
186
|
+
if (flags & 8) packet.streamId = block.readUInt32LE(at + HEADER_BYTES + count * 16 + ((flags & 2) ? 32 : 0));
|
|
187
|
+
if (flags & 16) packet.decodeFromIndex = block.readUInt32LE(at + HEADER_BYTES + count * 16 + ((flags & 2) ? 32 : 0) + ((flags & 8) ? 4 : 0));
|
|
188
|
+
if ((flags & 16) && !(flags & 32)) packet.decodeDependencyUnknown = true;
|
|
189
|
+
return packet;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setDecodeFromIndex(index, first) {
|
|
193
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= this.#length ||
|
|
194
|
+
!Number.isSafeInteger(first) || first < 0 || first > index || first > 0xffffffff) {
|
|
195
|
+
throw new TypeError("Invalid decoder dependency address.");
|
|
196
|
+
}
|
|
197
|
+
const [block, at] = this.#address(index);
|
|
198
|
+
const flags = block.readUInt32LE(at + 48), count = block.readUInt32LE(at + 52);
|
|
199
|
+
if (!(flags & 16)) throw new Error("Decoder dependency storage was not reserved.");
|
|
200
|
+
block.writeUInt32LE(first, at + HEADER_BYTES + count * 16 + ((flags & 2) ? 32 : 0) + ((flags & 8) ? 4 : 0));
|
|
201
|
+
block.writeUInt32LE(flags | 32, at + 48);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
extendLastPresentation(seconds) {
|
|
205
|
+
if (!this.#length) return;
|
|
206
|
+
const [block, at] = this.#address(this.#length - 1);
|
|
207
|
+
block.writeDoubleLE(block.readDoubleLE(at + 8) + seconds, at + 8);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
setDuration(index, seconds, derived = false) {
|
|
211
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= this.#length || !Number.isFinite(seconds) || seconds < 0) {
|
|
212
|
+
throw new TypeError("A packet duration requires an existing record and a finite nonnegative value.");
|
|
213
|
+
}
|
|
214
|
+
const [block, at] = this.#address(index);
|
|
215
|
+
block.writeDoubleLE(seconds, at + 8);
|
|
216
|
+
const flags = block.readUInt32LE(at + 48);
|
|
217
|
+
block.writeUInt32LE(derived ? flags | 4 : flags & ~4, at + 48);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
*[Symbol.iterator]() { for (let index = 0; index < this.#length; index++) yield this.at(index); }
|
|
221
|
+
map(callback) { return Array.from(this, callback); }
|
|
222
|
+
filter(callback) { return Array.from(this).filter(callback); }
|
|
223
|
+
some(callback) { for (const packet of this) if (callback(packet)) return true; return false; }
|
|
224
|
+
every(callback) { for (const packet of this) if (!callback(packet)) return false; return true; }
|
|
225
|
+
reduce(callback, initial) {
|
|
226
|
+
let value = initial;
|
|
227
|
+
for (const packet of this) value = callback(value, packet);
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
slice(start = 0, end = this.#length) {
|
|
231
|
+
if (start < 0) start = Math.max(0, this.#length + start);
|
|
232
|
+
if (end < 0) end = Math.max(0, this.#length + end);
|
|
233
|
+
const records = [];
|
|
234
|
+
for (let index = start; index < Math.min(end, this.#length); index++) records.push(this.at(index));
|
|
235
|
+
return records;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { IndexMemoryUnavailable } from "./memory-unavailable.js";
|
|
2
|
+
|
|
3
|
+
/** Reserve a retained metadata buffer before requesting its bytes. */
|
|
4
|
+
export class RetainedBytes {
|
|
5
|
+
#allocation;
|
|
6
|
+
#held = 0;
|
|
7
|
+
#closed = false;
|
|
8
|
+
|
|
9
|
+
constructor(memory = {}) {
|
|
10
|
+
this.#allocation = memory.forRecord?.(this, "declarations") ?? memory;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async read(bytes, read) {
|
|
14
|
+
if (this.#closed || this.#held) throw new Error("Metadata allocation is no longer available.");
|
|
15
|
+
if (this.#allocation.reserve?.(bytes) === false) throw new IndexMemoryUnavailable(bytes);
|
|
16
|
+
this.#held = bytes;
|
|
17
|
+
try {
|
|
18
|
+
const value = await read();
|
|
19
|
+
if (this.#closed) throw new Error("Metadata source was forgotten during its read.");
|
|
20
|
+
return value;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
this.#release();
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#release() {
|
|
28
|
+
if (!this.#held) return;
|
|
29
|
+
this.#allocation.release?.(this.#held);
|
|
30
|
+
this.#held = 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
dispose() {
|
|
34
|
+
if (this.#closed) return;
|
|
35
|
+
this.#closed = true;
|
|
36
|
+
this.#release();
|
|
37
|
+
this.#allocation.dispose?.();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IndexMemoryUnavailable } from "./memory-unavailable.js";
|
|
2
|
+
|
|
3
|
+
/** Retained declaration ranges reserve memory before reading and share retries. */
|
|
4
|
+
export class RetainedReads {
|
|
5
|
+
#allocation;
|
|
6
|
+
#ranges = new Map();
|
|
7
|
+
#held = 0;
|
|
8
|
+
#closed = false;
|
|
9
|
+
|
|
10
|
+
constructor(memory = {}) {
|
|
11
|
+
this.#allocation = memory.forRecord?.(this, "declarations") ?? memory;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async read(start, end, read) {
|
|
15
|
+
if (this.#closed) throw new Error("Metadata source was forgotten.");
|
|
16
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) {
|
|
17
|
+
throw new TypeError("Retained metadata requires an exact byte range.");
|
|
18
|
+
}
|
|
19
|
+
const key = `${start}:${end}`;
|
|
20
|
+
if (this.#ranges.has(key)) return this.#ranges.get(key);
|
|
21
|
+
const bytes = end - start + 1;
|
|
22
|
+
if (this.#allocation.reserve?.(bytes) === false) throw new IndexMemoryUnavailable(bytes);
|
|
23
|
+
this.#held += bytes;
|
|
24
|
+
const pending = Promise.resolve().then(() => read(start, end)).then(value => {
|
|
25
|
+
if (this.#closed) throw new Error("Metadata source was forgotten during its read.");
|
|
26
|
+
if (!value || value.length !== bytes) throw new Error("Retained metadata read is incomplete.");
|
|
27
|
+
if (value.byteOffset === 0 && value.buffer.byteLength === bytes && value.buffer instanceof ArrayBuffer) return value;
|
|
28
|
+
const owned = Buffer.allocUnsafeSlow(bytes);
|
|
29
|
+
owned.set(value);
|
|
30
|
+
return owned;
|
|
31
|
+
}).catch(error => {
|
|
32
|
+
this.#ranges.delete(key);
|
|
33
|
+
if (!this.#closed) {
|
|
34
|
+
this.#held -= bytes;
|
|
35
|
+
this.#allocation.release?.(bytes);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
});
|
|
39
|
+
this.#ranges.set(key, pending);
|
|
40
|
+
return pending;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
dispose() {
|
|
44
|
+
if (this.#closed) return;
|
|
45
|
+
this.#closed = true;
|
|
46
|
+
this.#ranges.clear();
|
|
47
|
+
this.#allocation.release?.(this.#held);
|
|
48
|
+
this.#held = 0;
|
|
49
|
+
this.#allocation.dispose?.();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** ATSC A/52 sync information gives exact frame length and sample count. */
|
|
2
|
+
export function ac3Frame(bytes) {
|
|
3
|
+
if (bytes.length < 7 || bytes[0] !== 0x0b || bytes[1] !== 0x77) throw new Error("AC-3 frame synchronization is invalid.");
|
|
4
|
+
const bitstreamId = bytes[5] >> 3;
|
|
5
|
+
if (bitstreamId > 16) throw new Error("AC-3 bitstream version is invalid.");
|
|
6
|
+
let position = 16;
|
|
7
|
+
const read = count => {
|
|
8
|
+
let value = 0;
|
|
9
|
+
for (let bit = 0; bit < count; bit++, position++) value = (value << 1) | ((bytes[position >> 3] >> (7 - (position & 7))) & 1);
|
|
10
|
+
return value;
|
|
11
|
+
};
|
|
12
|
+
let sampleRate, size, channelMode, lfe, samples, codecId;
|
|
13
|
+
const frequencies = [48000, 44100, 32000];
|
|
14
|
+
if (bitstreamId <= 10) {
|
|
15
|
+
read(16);
|
|
16
|
+
const frequency = read(2), sizeCode = read(6);
|
|
17
|
+
if (frequency === 3 || sizeCode > 37) throw new Error("AC-3 frame parameters are invalid.");
|
|
18
|
+
const rates = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640];
|
|
19
|
+
const bitrate = rates[sizeCode >> 1] * 1000;
|
|
20
|
+
size = frequency === 1 ? 2 * (Math.floor(bitrate * 1536 / (44100 * 16)) + (sizeCode & 1))
|
|
21
|
+
: bitrate * 1536 / (frequencies[frequency] * 8);
|
|
22
|
+
sampleRate = frequencies[frequency] / 2 ** Math.max(0, bitstreamId - 8);
|
|
23
|
+
read(5);
|
|
24
|
+
read(3);
|
|
25
|
+
channelMode = read(3);
|
|
26
|
+
if ((channelMode & 1) && channelMode !== 1) read(2);
|
|
27
|
+
if (channelMode & 4) read(2);
|
|
28
|
+
if (channelMode === 2) read(2);
|
|
29
|
+
lfe = read(1);
|
|
30
|
+
samples = 1536;
|
|
31
|
+
codecId = "ac3";
|
|
32
|
+
} else {
|
|
33
|
+
const streamType = read(2), substream = read(3);
|
|
34
|
+
if (streamType === 3) throw new Error("E-AC-3 stream type is reserved.");
|
|
35
|
+
if (streamType === 1 || substream !== 0) throw new Error("E-AC-3 dependent substreams require combined frame-set indexing.");
|
|
36
|
+
size = 2 * (read(11) + 1);
|
|
37
|
+
const frequency = read(2), blockCode = read(2);
|
|
38
|
+
sampleRate = frequency === 3 ? frequencies[blockCode] / 2 : frequencies[frequency];
|
|
39
|
+
if (!sampleRate || size < 7) throw new Error("E-AC-3 frame parameters are invalid.");
|
|
40
|
+
samples = 256 * (frequency === 3 ? 6 : [1, 2, 3, 6][blockCode]);
|
|
41
|
+
channelMode = read(3);
|
|
42
|
+
lfe = read(1);
|
|
43
|
+
codecId = "eac3";
|
|
44
|
+
}
|
|
45
|
+
return { size, sampleRate, channels: [2, 1, 2, 3, 3, 4, 4, 5][channelMode] + lfe, duration: samples / sampleRate, codecId };
|
|
46
|
+
}
|