@torrent-tv/proxy 2.91.0 → 2.92.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +3 -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 +66 -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/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,99 @@
|
|
|
1
|
+
import { Bits } from "./h264-configuration.js";
|
|
2
|
+
|
|
3
|
+
const SAMPLE_RATES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
|
|
4
|
+
|
|
5
|
+
/** LOAS frames retain their original addresses; payload bit positions describe raw AAC. */
|
|
6
|
+
export class LatmFrames {
|
|
7
|
+
#configuration = null;
|
|
8
|
+
|
|
9
|
+
read(bytes) {
|
|
10
|
+
if (bytes.length < 3 || bytes[0] !== 0x56 || (bytes[1] & 0xe0) !== 0xe0) throw new Error("AAC LOAS synchronization is invalid.");
|
|
11
|
+
const size = 3 + ((bytes[1] & 31) << 8) + bytes[2];
|
|
12
|
+
if (bytes.length < size) return { size };
|
|
13
|
+
const bits = new Bits(bytes.subarray(3, size), "AAC LATM");
|
|
14
|
+
if (!bits.uint(1)) {
|
|
15
|
+
const version = bits.uint(1), versionA = version ? bits.uint(1) : 0;
|
|
16
|
+
if (versionA) throw new Error("AAC LATM version A requires its own payload declarations.");
|
|
17
|
+
if (version) latmValue(bits);
|
|
18
|
+
const sameTime = bits.uint(1), subframes = bits.uint(6), programs = bits.uint(4), layers = bits.uint(3);
|
|
19
|
+
if (!sameTime || subframes || programs || layers) throw new Error("AAC LATM multiplexed layers require separate packet declarations.");
|
|
20
|
+
const ascLength = version ? latmValue(bits) : null, ascStart = bits.at;
|
|
21
|
+
const configuration = readAudioConfiguration(bits);
|
|
22
|
+
let ascEnd = bits.at;
|
|
23
|
+
if (ascLength !== null) {
|
|
24
|
+
if (ascEnd - ascStart > ascLength) throw new Error("AAC LATM decoder configuration exceeds its bit length.");
|
|
25
|
+
skip(bits, ascLength - (ascEnd - ascStart));
|
|
26
|
+
ascEnd = bits.at;
|
|
27
|
+
}
|
|
28
|
+
const lengthType = bits.uint(3);
|
|
29
|
+
if (lengthType !== 0) throw new Error("AAC LATM fixed or CELP frame length requires its own payload declarations.");
|
|
30
|
+
bits.uint(8);
|
|
31
|
+
if (bits.uint(1)) {
|
|
32
|
+
if (version) latmValue(bits);
|
|
33
|
+
else {
|
|
34
|
+
let more;
|
|
35
|
+
do { more = bits.uint(1); bits.uint(8); } while (more);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (bits.uint(1)) bits.uint(8);
|
|
39
|
+
const privateBytes = Buffer.alloc(Math.ceil((ascEnd - ascStart) / 8));
|
|
40
|
+
for (let bit = ascStart; bit < ascEnd; bit++) {
|
|
41
|
+
const value = (bits.bytes[bit >> 3] >> (7 - (bit & 7))) & 1;
|
|
42
|
+
privateBytes[(bit - ascStart) >> 3] |= value << (7 - ((bit - ascStart) & 7));
|
|
43
|
+
}
|
|
44
|
+
this.#configuration = { ...configuration, codecPrivateB64: privateBytes.toString("base64") };
|
|
45
|
+
}
|
|
46
|
+
if (!this.#configuration) throw new Error("AAC LATM payload precedes its stream configuration.");
|
|
47
|
+
let length = 0, part;
|
|
48
|
+
do { part = bits.uint(8); length += part; } while (part === 255);
|
|
49
|
+
if (!(length > 0) || bits.at + length * 8 > bits.bytes.length * 8) throw new Error("AAC LATM payload exceeds its LOAS frame.");
|
|
50
|
+
return { size, ...this.#configuration, codecId: "aac", bitOffset: bits.at + 24, bitLength: length * 8 };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readAudioConfiguration(bits) {
|
|
55
|
+
const start = bits.at;
|
|
56
|
+
let type = audioType(bits);
|
|
57
|
+
const coreRate = audioRate(bits);
|
|
58
|
+
let channels = bits.uint(4), sampleRate = coreRate;
|
|
59
|
+
const parametricStereo = type === 29;
|
|
60
|
+
if (type === 5 || parametricStereo) { sampleRate = audioRate(bits); type = audioType(bits); }
|
|
61
|
+
if (![1, 2, 3, 4].includes(type) || !coreRate || !sampleRate || channels > 7) {
|
|
62
|
+
throw new Error("AAC LATM requires a supported declared audio configuration.");
|
|
63
|
+
}
|
|
64
|
+
const shortFrame = bits.uint(1);
|
|
65
|
+
if (bits.uint(1)) bits.uint(14);
|
|
66
|
+
const extension = bits.uint(1);
|
|
67
|
+
channels = channels ? channels === 7 ? 8 : channels : programChannels(bits, start, coreRate);
|
|
68
|
+
if (extension && bits.uint(1)) throw new Error("AAC LATM extension version requires its own decoder declaration.");
|
|
69
|
+
return { sampleRate, channels: parametricStereo && channels === 1 ? 2 : channels,
|
|
70
|
+
duration: (shortFrame ? 960 : 1024) / coreRate };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function audioType(bits) { const type = bits.uint(5); return type === 31 ? 32 + bits.uint(6) : type; }
|
|
74
|
+
function audioRate(bits) { const index = bits.uint(4); return index === 15 ? bits.uint(24) : SAMPLE_RATES[index]; }
|
|
75
|
+
|
|
76
|
+
/** Program configuration declares every single, paired and LFE channel. */
|
|
77
|
+
function programChannels(bits, start, sampleRate) {
|
|
78
|
+
bits.uint(4); bits.uint(2);
|
|
79
|
+
if (SAMPLE_RATES[bits.uint(4)] !== sampleRate) throw new Error("AAC program sample rate differs from its decoder declaration.");
|
|
80
|
+
const front = bits.uint(4), side = bits.uint(4), back = bits.uint(4), lfe = bits.uint(2),
|
|
81
|
+
associated = bits.uint(3), coupled = bits.uint(4);
|
|
82
|
+
if (bits.uint(1)) bits.uint(4);
|
|
83
|
+
if (bits.uint(1)) bits.uint(4);
|
|
84
|
+
if (bits.uint(1)) bits.uint(3);
|
|
85
|
+
let channels = lfe;
|
|
86
|
+
for (let index = 0; index < front + side + back; index++) {
|
|
87
|
+
channels += bits.uint(1) ? 2 : 1;
|
|
88
|
+
bits.uint(4);
|
|
89
|
+
}
|
|
90
|
+
skip(bits, 4 * (lfe + associated));
|
|
91
|
+
skip(bits, 5 * coupled);
|
|
92
|
+
skip(bits, (8 - ((bits.at - start) % 8)) % 8);
|
|
93
|
+
skip(bits, bits.uint(8) * 8);
|
|
94
|
+
if (!channels) throw new Error("AAC program declares no output channels.");
|
|
95
|
+
return channels;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function latmValue(bits) { return bits.uint((bits.uint(2) + 1) * 8); }
|
|
99
|
+
function skip(bits, count) { for (let left = count; left > 0; left -= 32) bits.uint(Math.min(left, 32)); }
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { ElementReader } from "./ebml-stream.js";
|
|
2
|
+
import { readUint, readVint } from "./ebml-reader.js";
|
|
3
|
+
import { readBlockHeader } from "./matroska-clusters.js";
|
|
4
|
+
import { PacketIndex } from "./PacketIndex.js";
|
|
5
|
+
import { PacketRecords } from "./PacketRecords.js";
|
|
6
|
+
import { isMp3Codec } from "./mp3-packet-dependencies.js";
|
|
7
|
+
import { followingPacketTime } from "./following-packet-time.js";
|
|
8
|
+
|
|
9
|
+
const CLUSTER = 0x1f43b675;
|
|
10
|
+
const LEVEL_ONE = new Set([CLUSTER, 0x1c53bb6b, 0x114d9b74, 0x1549a966, 0x1654ae6b, 0x1043a770, 0x1941a469, 0x1254c367]);
|
|
11
|
+
|
|
12
|
+
/** Index frame addresses by reading structural headers, never frame bodies. */
|
|
13
|
+
export async function readMatroskaPackets({ readRange, fileSize, portionBytes, layout, tracks, durationSeconds, interval, packetMemory, state = {} }) {
|
|
14
|
+
const reader = new ElementReader({ read: readRange, fileSize, portionBytes });
|
|
15
|
+
const declared = new Map(tracks.filter(track => ["video", "audio", "subtitle"].includes(track.type)).map(track => [track.trackNumber, track]));
|
|
16
|
+
if (!state.packets) {
|
|
17
|
+
state.packets = new Map([...declared.keys()].map(id => [id, new PacketRecords(packetMemory)]));
|
|
18
|
+
state.at = layout.firstClusterAt ?? layout.segmentEnd;
|
|
19
|
+
}
|
|
20
|
+
const packets = state.packets;
|
|
21
|
+
if (intervalCovered(declared, packets, interval)) return buildIndex(declared, packets, durationSeconds, false, interval, packetMemory);
|
|
22
|
+
let at = state.at;
|
|
23
|
+
while (at < layout.segmentEnd) {
|
|
24
|
+
const element = state.cluster?.element ?? await validHeader(reader, at, layout.segmentEnd);
|
|
25
|
+
if (element.id !== CLUSTER) {
|
|
26
|
+
if (element.end === null) throw new Error("An unknown-sized Matroska element is not a Cluster.");
|
|
27
|
+
at = element.end;
|
|
28
|
+
state.at = at;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const limit = element.end ?? layout.segmentEnd;
|
|
32
|
+
if (!state.cluster) state.cluster = { element, cursor: element.dataOffset, clusterTicks: null };
|
|
33
|
+
let { cursor, clusterTicks } = state.cluster;
|
|
34
|
+
while (cursor < limit) {
|
|
35
|
+
const child = await validHeader(reader, cursor, limit);
|
|
36
|
+
if (element.end === null && LEVEL_ONE.has(child.id)) break;
|
|
37
|
+
if (child.end === null) throw new Error("Matroska Cluster child has unknown size.");
|
|
38
|
+
if (child.id === 0xe7) {
|
|
39
|
+
clusterTicks = readUint(await reader.bytes(child.dataOffset, child.size), 0, child.size);
|
|
40
|
+
} else if (child.id === 0xa3 || child.id === 0xa0) {
|
|
41
|
+
if (clusterTicks === null) throw new Error("Matroska block precedes its Cluster Timestamp.");
|
|
42
|
+
const block = child.id === 0xa3 ? { element: child, duration: null, referenced: false } : await blockGroup(reader, child);
|
|
43
|
+
if (block.element) {
|
|
44
|
+
const first = await reader.bytes(block.element.dataOffset, 1);
|
|
45
|
+
let trackWidth = 1;
|
|
46
|
+
while (trackWidth <= 8 && !(first[0] & (1 << (8 - trackWidth)))) trackWidth++;
|
|
47
|
+
if (trackWidth > 8 || trackWidth + 3 > block.element.size) throw new Error("Matroska block header is truncated.");
|
|
48
|
+
const head = await reader.bytes(block.element.dataOffset, trackWidth + 3);
|
|
49
|
+
const header = readBlockHeader(head, 0, head.length);
|
|
50
|
+
const track = header && declared.get(header.trackNumber);
|
|
51
|
+
if (!header) throw new Error("Matroska block header is truncated.");
|
|
52
|
+
if (!track) {
|
|
53
|
+
if (!tracks.some(track => track.trackNumber === header.trackNumber)) throw new Error("Matroska block names an undeclared media track.");
|
|
54
|
+
cursor = child.end;
|
|
55
|
+
state.cluster.cursor = cursor;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const ranges = await frameRanges(reader, block.element, header);
|
|
59
|
+
const duration = block.duration === null ? track.defaultDurationSeconds ?? 0 : block.duration * layout.secondsPerTick / ranges.length;
|
|
60
|
+
if (ranges.length > 1 && !(duration > 0)) throw new Error("Matroska laced frames need a declared or codec-derived duration.");
|
|
61
|
+
const pts = (clusterTicks + header.relativeTicks * (track.timestampScale ?? 1)) * layout.secondsPerTick - (track.codecDelaySeconds ?? 0);
|
|
62
|
+
const records = packets.get(header.trackNumber);
|
|
63
|
+
const previousLength = records.length;
|
|
64
|
+
try {
|
|
65
|
+
for (let frame = 0; frame < ranges.length; frame++) {
|
|
66
|
+
const padding = paddingForFrame(block.discardPaddingSeconds ?? 0, duration, ranges.length, frame);
|
|
67
|
+
records.push({ pts: pts + frame * duration, duration,
|
|
68
|
+
...(track.type === "audio" && isMp3Codec(track.codecId)
|
|
69
|
+
? { decodeFromIndex: records.length, decodeDependencyUnknown: true } : {}),
|
|
70
|
+
...(padding ? { discardPaddingSeconds: padding } : {}),
|
|
71
|
+
keyframe: child.id === 0xa3 ? !!(header.flags & 0x80) : !block.referenced,
|
|
72
|
+
ranges: [ranges[frame]] });
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
records.length = previousLength;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else if (child.id === 0xaf) throw new Error("Matroska media blocks are encrypted.");
|
|
80
|
+
cursor = child.end;
|
|
81
|
+
state.cluster.cursor = cursor;
|
|
82
|
+
state.cluster.clusterTicks = clusterTicks;
|
|
83
|
+
if (intervalCovered(declared, packets, interval)) return buildIndex(declared, packets, durationSeconds, false, interval, packetMemory);
|
|
84
|
+
}
|
|
85
|
+
if (cursor <= at) throw new Error("Matroska packet walk did not advance.");
|
|
86
|
+
at = element.end ?? cursor;
|
|
87
|
+
state.at = at;
|
|
88
|
+
state.cluster = null;
|
|
89
|
+
}
|
|
90
|
+
return buildIndex(declared, packets, durationSeconds, true, undefined, packetMemory);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function paddingForFrame(seconds, duration, count, frame) {
|
|
94
|
+
if (!seconds || count === 1) return seconds;
|
|
95
|
+
const padding = Math.round(seconds * 1e9), frameTime = Math.round(duration * 1e9);
|
|
96
|
+
if (Math.abs(padding) > frameTime * count) throw new Error("Matroska DiscardPadding exceeds its laced block duration.");
|
|
97
|
+
const preceding = padding < 0 ? frame : count - frame - 1;
|
|
98
|
+
return Math.sign(padding) * Math.max(0, Math.min(frameTime, Math.abs(padding) - preceding * frameTime)) / 1e9;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function intervalCovered(declared, packets, interval) {
|
|
102
|
+
if (!interval || !Number.isFinite(interval.to)) return false;
|
|
103
|
+
const requested = [...declared].filter(([id, track]) => interval.trackIds
|
|
104
|
+
? interval.trackIds.includes(id) : ["video", "audio"].includes(track.type));
|
|
105
|
+
return requested.length > 0 && requested.every(([id, track]) => {
|
|
106
|
+
const depth = track.type === "video" ? track.reorderDepth : 0;
|
|
107
|
+
if (!Number.isSafeInteger(depth) || depth < 0) return false;
|
|
108
|
+
const stream = packets.get(id);
|
|
109
|
+
return stream.length > depth && stream.slice(-(depth + 1)).every(packet => packet.pts >= interval.to);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildIndex(declared, packets, durationSeconds, complete, interval, packetMemory) {
|
|
114
|
+
const index = new PacketIndex();
|
|
115
|
+
for (const [id, track] of declared) {
|
|
116
|
+
index.declareTrack(id, { type: track.type, codecId: track.codecId, codecRanges: track.codecRanges ?? [], prerollSeconds: track.seekPrerollSeconds ?? 0, reorderDepth: track.reorderDepth ?? 0 });
|
|
117
|
+
const stream = packets.get(id);
|
|
118
|
+
let needsFollowing = false;
|
|
119
|
+
for (let position = 0; position < stream.length; position++) {
|
|
120
|
+
if (stream.durationAt(position) === 0 || stream.derivedDurationAt(position)) { needsFollowing = true; break; }
|
|
121
|
+
}
|
|
122
|
+
if (needsFollowing) {
|
|
123
|
+
const endFor = followingPacketTime(stream, complete ? durationSeconds : null, packetMemory);
|
|
124
|
+
try { for (let position = 0; position < stream.length; position++) {
|
|
125
|
+
if (stream.durationAt(position) !== 0 && !stream.derivedDurationAt(position)) continue;
|
|
126
|
+
const pts = stream.ptsAt(position), end = endFor(pts);
|
|
127
|
+
stream.setDuration(position, Number.isFinite(end) && end > pts ? end - pts : 0, true);
|
|
128
|
+
} } finally { endFor.dispose(); }
|
|
129
|
+
}
|
|
130
|
+
index.sharePackets(id, stream);
|
|
131
|
+
if (complete) index.complete(id);
|
|
132
|
+
else if (interval.trackIds ? interval.trackIds.includes(id) : ["video", "audio"].includes(track.type)) {
|
|
133
|
+
index.coverThrough(id, interval.to);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return index;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function validHeader(reader, at, limit) {
|
|
140
|
+
const element = await reader.header(at, limit);
|
|
141
|
+
if (!element || (element.end !== null && element.end > limit)) throw new Error("Matroska element exceeds its parent.");
|
|
142
|
+
return element;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function blockGroup(reader, parent) {
|
|
146
|
+
const result = { element: null, duration: null, referenced: false };
|
|
147
|
+
for (let at = parent.dataOffset; at < parent.end;) {
|
|
148
|
+
const child = await validHeader(reader, at, parent.end);
|
|
149
|
+
if (child.end === null) throw new Error("Matroska BlockGroup child has unknown size.");
|
|
150
|
+
if (child.id === 0xa1) {
|
|
151
|
+
if (result.element) throw new Error("Matroska BlockGroup declares multiple Blocks.");
|
|
152
|
+
result.element = child;
|
|
153
|
+
}
|
|
154
|
+
if (child.id === 0x9b) result.duration = readUint(await reader.bytes(child.dataOffset, child.size), 0, child.size);
|
|
155
|
+
if (child.id === 0x75a2) {
|
|
156
|
+
if (child.size < 1 || child.size > 8) throw new Error("Matroska DiscardPadding has an invalid integer size.");
|
|
157
|
+
const bytes = await reader.bytes(child.dataOffset, child.size);
|
|
158
|
+
const signed = Buffer.alloc(8, bytes[0] & 128 ? 255 : 0);
|
|
159
|
+
bytes.copy(signed, 8 - bytes.length);
|
|
160
|
+
const nanoseconds = Number(signed.readBigInt64BE());
|
|
161
|
+
if (!Number.isSafeInteger(nanoseconds)) throw new Error("Matroska DiscardPadding exceeds its exact time range.");
|
|
162
|
+
result.discardPaddingSeconds = nanoseconds / 1e9;
|
|
163
|
+
}
|
|
164
|
+
if (child.id === 0xfb) result.referenced = true;
|
|
165
|
+
if (child.id === 0xa4) throw new Error("Matroska packet changes CodecState; its packet configuration must be indexed.");
|
|
166
|
+
at = child.end;
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Decode all lacing forms without copying a frame to inspect its length. */
|
|
172
|
+
async function frameRanges(reader, element, header) {
|
|
173
|
+
let at = element.dataOffset + header.dataOffset;
|
|
174
|
+
const lacing = header.flags & 6;
|
|
175
|
+
if (!lacing) return [[at, element.end - 1]];
|
|
176
|
+
const count = (await reader.bytes(at++, 1))[0] + 1;
|
|
177
|
+
const sizes = [];
|
|
178
|
+
if (lacing === 2) {
|
|
179
|
+
for (let frame = 0; frame < count - 1; frame++) {
|
|
180
|
+
let size = 0, byte;
|
|
181
|
+
do {
|
|
182
|
+
if (at >= element.end) throw new Error("Matroska Xiph lace is truncated.");
|
|
183
|
+
byte = (await reader.bytes(at++, 1))[0];
|
|
184
|
+
size += byte;
|
|
185
|
+
} while (byte === 255);
|
|
186
|
+
sizes.push(size);
|
|
187
|
+
}
|
|
188
|
+
} else if (lacing === 6) {
|
|
189
|
+
for (let frame = 0; frame < count - 1; frame++) {
|
|
190
|
+
const bytes = await reader.bytes(at, Math.min(8, element.end - at));
|
|
191
|
+
const size = readVint(bytes, 0, false);
|
|
192
|
+
if (!size || size.value === null) throw new Error("Matroska EBML lace is truncated.");
|
|
193
|
+
at += size.length;
|
|
194
|
+
sizes.push(frame === 0 ? Number(size.value) : sizes.at(-1) + Number(size.value) - (2 ** (7 * size.length - 1) - 1));
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
if ((element.end - at) % count !== 0) throw new Error("Matroska fixed lace does not divide its payload.");
|
|
198
|
+
for (let frame = 0; frame < count - 1; frame++) sizes.push((element.end - at) / count);
|
|
199
|
+
}
|
|
200
|
+
sizes.push(element.end - at - sizes.reduce((sum, size) => sum + size, 0));
|
|
201
|
+
return sizes.map(size => {
|
|
202
|
+
if (!Number.isSafeInteger(size) || size <= 0 || at + size > element.end) throw new Error("Matroska lace exceeds its frame data.");
|
|
203
|
+
const range = [at, at + size - 1];
|
|
204
|
+
at += size;
|
|
205
|
+
return range;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** A resource shortage is pending work, not evidence of damaged media. */
|
|
2
|
+
export class IndexMemoryUnavailable extends Error {
|
|
3
|
+
constructor(bytes) {
|
|
4
|
+
super(`Packet index requires ${bytes} additional allocation bytes.`);
|
|
5
|
+
this.name = "IndexMemoryUnavailable";
|
|
6
|
+
this.bytes = bytes;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mpegAudioFrame } from "./mpeg-audio-frame.js";
|
|
2
|
+
|
|
3
|
+
export function isMp3Codec(codecId) { return ["mp3", "A_MPEG/L3", ".mp3"].includes(codecId); }
|
|
4
|
+
|
|
5
|
+
/** Inspect frame headers and side information, never their compressed bodies. */
|
|
6
|
+
export async function mp3PacketFacts(packet, readRange) {
|
|
7
|
+
const size = packet.ranges.reduce((sum, [start, end]) => sum + end - start + 1, 0);
|
|
8
|
+
let at = 0, capacity = 0, required = 0;
|
|
9
|
+
while (at < size) {
|
|
10
|
+
const header = await prefix(packet.ranges, at, 4, readRange);
|
|
11
|
+
const frame = mpegAudioFrame(header);
|
|
12
|
+
if (frame.codecId !== "mp3" || at + frame.size > size) throw new Error("MP3 packet frame addresses are inconsistent.");
|
|
13
|
+
const mpeg1 = ((header[1] >> 3) & 3) === 3, mono = header[3] >> 6 === 3;
|
|
14
|
+
const headerBytes = 4 + ((header[1] & 1) ? 0 : 2);
|
|
15
|
+
const sideBytes = mpeg1 ? mono ? 17 : 32 : mono ? 9 : 17;
|
|
16
|
+
if (frame.size < headerBytes + sideBytes) throw new Error("MP3 frame is smaller than its side information.");
|
|
17
|
+
const bytes = await prefix(packet.ranges, at + headerBytes, sideBytes, readRange);
|
|
18
|
+
const before = mpeg1 ? (bytes[0] << 1) | (bytes[1] >> 7) : bytes[0];
|
|
19
|
+
required = Math.max(required, before - capacity);
|
|
20
|
+
capacity += frame.size - headerBytes - sideBytes;
|
|
21
|
+
at += frame.size;
|
|
22
|
+
}
|
|
23
|
+
return { required, capacity };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function prefix(ranges, offset, length, readRange) {
|
|
27
|
+
const bytes = Buffer.allocUnsafeSlow(length);
|
|
28
|
+
let copied = 0;
|
|
29
|
+
for (const [start, end] of ranges) {
|
|
30
|
+
const size = end - start + 1;
|
|
31
|
+
if (offset >= size) { offset -= size; continue; }
|
|
32
|
+
const wanted = Math.min(length - copied, size - offset);
|
|
33
|
+
const value = await readRange(start + offset, start + offset + wanted - 1);
|
|
34
|
+
if (!value || value.length !== wanted) throw new Error("MP3 header read is incomplete.");
|
|
35
|
+
bytes.set(value, copied);
|
|
36
|
+
copied += wanted;
|
|
37
|
+
if (copied === length) return bytes;
|
|
38
|
+
offset = 0;
|
|
39
|
+
}
|
|
40
|
+
throw new Error("MP3 packet side information exceeds its byte ranges.");
|
|
41
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { PacketIndex } from "./PacketIndex.js";
|
|
2
|
+
import { PacketRecords } from "./PacketRecords.js";
|
|
3
|
+
import { RetainedBytes } from "./RetainedBytes.js";
|
|
4
|
+
import { mpegAudioFrame } from "./mpeg-audio-frame.js";
|
|
5
|
+
import { isMp3Codec } from "./mp3-packet-dependencies.js";
|
|
6
|
+
|
|
7
|
+
/** Fragment sample tables retain exact payload addresses and declared clocks. */
|
|
8
|
+
export async function readMp4Fragments({ readRange, fileSize, tracks, memory, state, firstTrackIds = null }) {
|
|
9
|
+
if (!state.frames) {
|
|
10
|
+
state.frames = new Map(tracks.map(track => [track.id, new PacketRecords(memory)]));
|
|
11
|
+
state.clocks = new Map();
|
|
12
|
+
state.at = 0;
|
|
13
|
+
state.media = [];
|
|
14
|
+
}
|
|
15
|
+
while (state.at < fileSize) {
|
|
16
|
+
const at = state.at;
|
|
17
|
+
if (fileSize - at < 8) throw new Error("MP4 ends inside a fragment box header.");
|
|
18
|
+
const header = await readRange(at, at + 7);
|
|
19
|
+
const type = header.toString("latin1", 4, 8);
|
|
20
|
+
const width = header.readUInt32BE() === 1 ? 16 : 8;
|
|
21
|
+
const size = width === 16 ? Number((await readRange(at + 8, at + 15)).readBigUInt64BE()) :
|
|
22
|
+
header.readUInt32BE() || fileSize - at;
|
|
23
|
+
if (!Number.isSafeInteger(size) || size < width || size > fileSize - at) throw new Error("MP4 fragment box exceeds the file.");
|
|
24
|
+
if (type === "mdat") state.media.push([at + width, at + size]);
|
|
25
|
+
if (type === "moof") {
|
|
26
|
+
const allocation = new RetainedBytes(memory);
|
|
27
|
+
const lengths = new Map([...state.frames].map(([id, frames]) => [id, frames.length]));
|
|
28
|
+
const clocks = new Map(state.clocks);
|
|
29
|
+
try {
|
|
30
|
+
const bytes = await allocation.read(size, () => readRange(at, at + size - 1));
|
|
31
|
+
let precedingEnd = at;
|
|
32
|
+
const fragments = children(bytes, width, bytes.length, "traf");
|
|
33
|
+
if (!fragments.length) throw new Error("MP4 fragment has no track runs.");
|
|
34
|
+
for (const traf of fragments) {
|
|
35
|
+
const tfhd = child(bytes, traf.dataOffset, traf.end, "tfhd");
|
|
36
|
+
if (!tfhd || tfhd.dataOffset + 8 > tfhd.end) throw new Error("MP4 fragment has no complete track header.");
|
|
37
|
+
const flags = bytes.readUIntBE(tfhd.dataOffset + 1, 3);
|
|
38
|
+
const id = bytes.readUInt32BE(tfhd.dataOffset + 4);
|
|
39
|
+
const track = tracks.find(track => track.id === id);
|
|
40
|
+
if (!track) throw new Error("MP4 fragment names an undeclared track.");
|
|
41
|
+
let cursor = tfhd.dataOffset + 8;
|
|
42
|
+
const take = width => {
|
|
43
|
+
if (cursor + width > tfhd.end) throw new Error("MP4 fragment defaults are truncated.");
|
|
44
|
+
const value = width === 8 ? Number(bytes.readBigUInt64BE(cursor)) : bytes.readUInt32BE(cursor);
|
|
45
|
+
cursor += width;
|
|
46
|
+
if (!Number.isSafeInteger(value)) throw new Error("MP4 fragment default exceeds its integer range.");
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
const base = flags & 1 ? take(8) : flags & 0x20000 ? at : precedingEnd;
|
|
50
|
+
const description = flags & 2 ? take(4) : track.description;
|
|
51
|
+
if (description !== 1) throw new Error("MP4 fragment changes its sample description.");
|
|
52
|
+
const defaultDuration = flags & 8 ? take(4) : track.duration;
|
|
53
|
+
const defaultSize = flags & 16 ? take(4) : track.size;
|
|
54
|
+
const defaultFlags = flags & 32 ? take(4) : track.flags;
|
|
55
|
+
if (cursor !== tfhd.end) throw new Error("MP4 fragment defaults contain undeclared fields.");
|
|
56
|
+
const tfdt = child(bytes, traf.dataOffset, traf.end, "tfdt");
|
|
57
|
+
let decode = state.clocks.get(id) ?? 0;
|
|
58
|
+
if (tfdt) {
|
|
59
|
+
const wide = bytes[tfdt.dataOffset] === 1;
|
|
60
|
+
if (bytes[tfdt.dataOffset] > 1 || tfdt.dataOffset + (wide ? 12 : 8) > tfdt.end) throw new Error("MP4 fragment decode clock is truncated.");
|
|
61
|
+
decode = wide ? Number(bytes.readBigUInt64BE(tfdt.dataOffset + 4)) : bytes.readUInt32BE(tfdt.dataOffset + 4);
|
|
62
|
+
if (!Number.isSafeInteger(decode)) throw new Error("MP4 fragment decode clock exceeds its integer range.");
|
|
63
|
+
}
|
|
64
|
+
let address = base;
|
|
65
|
+
for (const trun of children(bytes, traf.dataOffset, traf.end, "trun")) {
|
|
66
|
+
if (trun.dataOffset + 8 > trun.end || bytes[trun.dataOffset] > 1) throw new Error("MP4 fragment run header is invalid.");
|
|
67
|
+
const runFlags = bytes.readUIntBE(trun.dataOffset + 1, 3);
|
|
68
|
+
const count = bytes.readUInt32BE(trun.dataOffset + 4);
|
|
69
|
+
let field = trun.dataOffset + 8;
|
|
70
|
+
const read = signed => {
|
|
71
|
+
if (field + 4 > trun.end) throw new Error("MP4 fragment sample field is truncated.");
|
|
72
|
+
const value = signed ? bytes.readInt32BE(field) : bytes.readUInt32BE(field);
|
|
73
|
+
field += 4;
|
|
74
|
+
return value;
|
|
75
|
+
};
|
|
76
|
+
if (runFlags & 1) address = base + read(true);
|
|
77
|
+
const firstFlags = runFlags & 4 ? read(false) : defaultFlags;
|
|
78
|
+
for (let sample = 0; sample < count; sample++) {
|
|
79
|
+
const duration = runFlags & 0x100 ? read(false) : defaultDuration;
|
|
80
|
+
const size = runFlags & 0x200 ? read(false) : defaultSize;
|
|
81
|
+
const flags = runFlags & 0x400 ? read(false) : sample === 0 ? firstFlags : defaultFlags;
|
|
82
|
+
const composition = runFlags & 0x800 ? read(bytes[trun.dataOffset] === 1) : 0;
|
|
83
|
+
if (!(duration > 0) || !(size > 0) || !Number.isSafeInteger(address) || address < 0 ||
|
|
84
|
+
address + size > fileSize || !Number.isSafeInteger(decode + duration)) throw new Error("MP4 fragment sample timing or address is invalid.");
|
|
85
|
+
if (track.track.codecId === "mpeg_audio") {
|
|
86
|
+
if (size < 4) throw new Error("MP4 MPEG audio first sample header is truncated.");
|
|
87
|
+
const facts = mpegAudioFrame(await readRange(address, address + 3));
|
|
88
|
+
track.track.codecId = facts.codecId;
|
|
89
|
+
track.track.channels = facts.channels;
|
|
90
|
+
track.track.samplingFrequency = facts.sampleRate;
|
|
91
|
+
}
|
|
92
|
+
state.frames.get(id).push({ pts: (decode + composition) / track.scale + track.shift,
|
|
93
|
+
dts: decode / track.scale + track.shift, duration: duration / track.scale,
|
|
94
|
+
...(isMp3Codec(track.track.codecId) ? { decodeFromIndex: state.frames.get(id).length, decodeDependencyUnknown: true } : {}),
|
|
95
|
+
keyframe: !(flags & 0x10000), ranges: [[address, address + size - 1]] });
|
|
96
|
+
decode += duration;
|
|
97
|
+
address += size;
|
|
98
|
+
}
|
|
99
|
+
if (field !== trun.end) throw new Error("MP4 fragment sample count differs from its run size.");
|
|
100
|
+
}
|
|
101
|
+
state.clocks.set(id, decode);
|
|
102
|
+
precedingEnd = address;
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
for (const [id, length] of lengths) state.frames.get(id).length = length;
|
|
106
|
+
state.clocks = clocks;
|
|
107
|
+
throw error;
|
|
108
|
+
} finally { allocation.dispose(); }
|
|
109
|
+
}
|
|
110
|
+
state.at += size;
|
|
111
|
+
if (firstTrackIds?.every(id => state.frames.get(id)?.length > 0)) return null;
|
|
112
|
+
}
|
|
113
|
+
const index = new PacketIndex({ packetMemory: memory });
|
|
114
|
+
for (const track of tracks) {
|
|
115
|
+
const frames = state.frames.get(track.id);
|
|
116
|
+
for (const packet of frames) for (const [start, end] of packet.ranges) {
|
|
117
|
+
if (!state.media.some(([from, to]) => from <= start && end < to)) throw new Error("MP4 fragment sample is outside its media data boxes.");
|
|
118
|
+
}
|
|
119
|
+
index.declareTrack(track.id, { type: track.track.type, codecId: track.track.codecId,
|
|
120
|
+
prerollSeconds: track.track.seekPrerollSeconds ?? 0, reorderDepth: track.track.reorderDepth ?? 0 });
|
|
121
|
+
index.sharePackets(track.id, frames);
|
|
122
|
+
index.complete(track.id);
|
|
123
|
+
}
|
|
124
|
+
return index;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function children(bytes, start, end, wanted) {
|
|
128
|
+
const found = [];
|
|
129
|
+
for (let at = start; at < end;) {
|
|
130
|
+
if (end - at < 8) throw new Error("MP4 fragment child header is truncated.");
|
|
131
|
+
const stated = bytes.readUInt32BE(at);
|
|
132
|
+
const width = stated === 1 ? 16 : 8;
|
|
133
|
+
if (end - at < width) throw new Error("MP4 fragment extended child header is truncated.");
|
|
134
|
+
const size = stated === 1 ? Number(bytes.readBigUInt64BE(at + 8)) : stated || end - at;
|
|
135
|
+
if (!Number.isSafeInteger(size) || size < width || size > end - at) {
|
|
136
|
+
throw new Error("MP4 fragment child exceeds its parent.");
|
|
137
|
+
}
|
|
138
|
+
if (bytes.toString("latin1", at + 4, at + 8) === wanted) found.push({ dataOffset: at + width, end: at + size });
|
|
139
|
+
at += size;
|
|
140
|
+
}
|
|
141
|
+
return found;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function child(bytes, start, end, wanted) {
|
|
145
|
+
const found = children(bytes, start, end, wanted);
|
|
146
|
+
if (found.length > 1) throw new Error(`MP4 fragment repeats ${wanted}.`);
|
|
147
|
+
return found[0] ?? null;
|
|
148
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function mpegAudioFrame(bytes) {
|
|
2
|
+
if (bytes[0] !== 0xff || (bytes[1] & 0xe0) !== 0xe0) throw new Error("MPEG audio lost frame synchronization.");
|
|
3
|
+
const version = (bytes[1] >> 3) & 3, layer = (bytes[1] >> 1) & 3;
|
|
4
|
+
const rateIndex = bytes[2] >> 4, sampleIndex = (bytes[2] >> 2) & 3;
|
|
5
|
+
if (version === 1 || !layer || !rateIndex || rateIndex === 15 || sampleIndex === 3) throw new Error("MPEG audio frame parameters are invalid.");
|
|
6
|
+
const mpeg1 = version === 3;
|
|
7
|
+
const rates = mpeg1 ? layer === 3 ? [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448] :
|
|
8
|
+
layer === 2 ? [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384] :
|
|
9
|
+
[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] :
|
|
10
|
+
layer === 3 ? [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256] :
|
|
11
|
+
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160];
|
|
12
|
+
const sampleRate = [44100, 48000, 32000][sampleIndex] / (mpeg1 ? 1 : version === 2 ? 2 : 4);
|
|
13
|
+
const bitrate = rates[rateIndex] * 1000, padding = (bytes[2] >> 1) & 1;
|
|
14
|
+
const samples = layer === 3 ? 384 : layer === 1 && !mpeg1 ? 576 : 1152;
|
|
15
|
+
const size = layer === 3 ? (Math.floor(12 * bitrate / sampleRate) + padding) * 4 :
|
|
16
|
+
Math.floor((layer === 1 && !mpeg1 ? 72 : 144) * bitrate / sampleRate) + padding;
|
|
17
|
+
if (size < 4) throw new Error("MPEG audio frame is smaller than its header.");
|
|
18
|
+
return { size, sampleRate, codecId: `mp${4 - layer}`, duration: samples / sampleRate, channels: bytes[3] >> 6 === 3 ? 1 : 2 };
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Layer III decoder input follows its declared main_data_begin byte references. */
|
|
2
|
+
export class MpegAudioReservoir {
|
|
3
|
+
#history = [];
|
|
4
|
+
|
|
5
|
+
prepare(bytes, frameSize, frameIndex) {
|
|
6
|
+
const version = (bytes[1] >> 3) & 3, layer = (bytes[1] >> 1) & 3;
|
|
7
|
+
if (layer !== 1) return null;
|
|
8
|
+
if (version === 1) throw new Error("MPEG audio version is invalid.");
|
|
9
|
+
const mono = bytes[3] >> 6 === 3, mpeg1 = version === 3;
|
|
10
|
+
const header = 4 + ((bytes[1] & 1) ? 0 : 2);
|
|
11
|
+
const sideBytes = mpeg1 ? mono ? 17 : 32 : mono ? 9 : 17;
|
|
12
|
+
if (bytes.length < header + sideBytes || frameSize < header + sideBytes) throw new Error("MP3 side information is truncated.");
|
|
13
|
+
const required = mpeg1 ? (bytes[header] << 1) | (bytes[header + 1] >> 7) : bytes[header];
|
|
14
|
+
let available = 0, decodeFromIndex = frameIndex;
|
|
15
|
+
for (let at = this.#history.length - 1; at >= 0 && available < required; at--) {
|
|
16
|
+
available += this.#history[at].bytes;
|
|
17
|
+
decodeFromIndex = this.#history[at].index;
|
|
18
|
+
}
|
|
19
|
+
if (available < required) throw new Error("MP3 bit reservoir refers to bytes before the available source frames.");
|
|
20
|
+
const capacity = frameSize - header - sideBytes;
|
|
21
|
+
const history = capacity ? [...this.#history, { index: frameIndex, bytes: capacity }] : [...this.#history];
|
|
22
|
+
let total = history.reduce((sum, frame) => sum + frame.bytes, 0);
|
|
23
|
+
const maximum = mpeg1 ? 511 : 255;
|
|
24
|
+
while (history.length > 1 && total - history[0].bytes >= maximum) total -= history.shift().bytes;
|
|
25
|
+
return { decodeFromIndex, commit: () => { this.#history = history; } };
|
|
26
|
+
}
|
|
27
|
+
}
|