@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.
Files changed (145) hide show
  1. package/bin/cli.js +3 -2
  2. package/package.json +18 -2
  3. package/routes/api/playback-plan/post.js +21 -4
  4. package/routes/api/sources/files/get.js +20 -28
  5. package/routes/api/sources/post.js +4 -2
  6. package/routes/api/sources/stats/get.js +4 -11
  7. package/routes/api/sources/viewer/post.js +16 -0
  8. package/routes/api/sources/warm/post.js +6 -215
  9. package/routes/api/subtitles/get.js +22 -125
  10. package/routes/api/subtitles/post.js +26 -0
  11. package/routes/api/transcode-sessions/post.js +24 -2
  12. package/routes/media/get.js +80 -0
  13. package/routes/stream/get.js +26 -94
  14. package/routes/transcode/audio-warm/get.js +9 -21
  15. package/routes/transcode/variant-warm/get.js +8 -14
  16. package/server.js +410 -46
  17. package/services/encode/AdmittedInput.js +136 -0
  18. package/services/encode/CushionReport.js +1 -76
  19. package/services/encode/EncodeInputs.js +142 -0
  20. package/services/encode/EncodeOrchestrator.js +4 -1
  21. package/services/encode/EncodeRun.js +13 -4
  22. package/services/encode/EncodeRuns.js +132 -52
  23. package/services/encode/InputFailures.js +37 -0
  24. package/services/encode/MatroskaInput.js +158 -0
  25. package/services/encode/OutputOpening.js +16 -64
  26. package/services/encode/Renditions.js +1 -17
  27. package/services/encode/admitted-command.js +60 -0
  28. package/services/encode/annex-b-input.js +37 -0
  29. package/services/encode/bit-payload.js +29 -0
  30. package/services/encode/output/OutputCatalog.js +1 -1
  31. package/services/encode/output/Timeline.js +11 -0
  32. package/services/encode/pcm-normalization.js +30 -0
  33. package/services/encode/piece-completeness.js +54 -1
  34. package/services/encode/quality/HostLoad.js +19 -73
  35. package/services/encode/quality/QualityOffer.js +1 -1
  36. package/services/encode/run-command.js +1 -589
  37. package/services/encode/segment-formats/mp4-boxes.js +3 -1
  38. package/services/encode/segment-formats/presentation-segment.js +114 -0
  39. package/services/media/ContainerOrchestrator.js +118 -48
  40. package/services/media/KeyframeTables.js +16 -115
  41. package/services/media/MediaReadRequests.js +120 -0
  42. package/services/media/ProbeRequests.js +135 -0
  43. package/services/media/SegmentInputs.js +48 -0
  44. package/services/media/SourceFile.js +6 -0
  45. package/services/media/SubtitleOrchestrator.js +190 -4
  46. package/services/media/await-plan.js +47 -0
  47. package/services/media/container/AsfContainer.js +217 -0
  48. package/services/media/container/AviContainer.js +343 -34
  49. package/services/media/container/CadencedPictureOrder.js +45 -0
  50. package/services/media/container/Container.js +7 -3
  51. package/services/media/container/ContainerFactory.js +10 -3
  52. package/services/media/container/FfprobeContainer.js +222 -0
  53. package/services/media/container/MatroskaContainer.js +72 -46
  54. package/services/media/container/Mp4Container.js +472 -139
  55. package/services/media/container/MpegPsContainer.js +281 -0
  56. package/services/media/container/MpegTsContainer.js +277 -0
  57. package/services/media/container/PacketIndex.js +270 -0
  58. package/services/media/container/PacketRecords.js +237 -0
  59. package/services/media/container/RetainedBytes.js +39 -0
  60. package/services/media/container/RetainedReads.js +51 -0
  61. package/services/media/container/ac3-frame.js +46 -0
  62. package/services/media/container/asf-packets.js +153 -0
  63. package/services/media/container/avc-picture-order.js +50 -0
  64. package/services/media/container/avi-open-dml.js +47 -0
  65. package/services/media/container/dts-frame.js +51 -0
  66. package/services/media/container/ebml-stream.js +12 -1
  67. package/services/media/container/ffprobe-record.js +45 -0
  68. package/services/media/container/flv-packet-ranges.js +27 -0
  69. package/services/media/container/following-packet-time.js +37 -0
  70. package/services/media/container/h264-configuration.js +183 -0
  71. package/services/media/container/h264-elementary-index.js +245 -0
  72. package/services/media/container/hevc-configuration.js +165 -0
  73. package/services/media/container/hevc-elementary-index.js +132 -0
  74. package/services/media/container/hevc-picture-order.js +66 -0
  75. package/services/media/container/latm-frame.js +99 -0
  76. package/services/media/container/matroska-packets.js +207 -0
  77. package/services/media/container/memory-unavailable.js +8 -0
  78. package/services/media/container/mp3-packet-dependencies.js +41 -0
  79. package/services/media/container/mp4-fragments.js +148 -0
  80. package/services/media/container/mpeg-audio-frame.js +19 -0
  81. package/services/media/container/mpeg-audio-reservoir.js +27 -0
  82. package/services/media/container/mpeg-elementary-index.js +196 -0
  83. package/services/media/container/mpeg-sequence-reader.js +69 -0
  84. package/services/media/container/mpeg-ts-packets.js +93 -0
  85. package/services/media/container/mpeg-video-configuration.js +50 -0
  86. package/services/media/container/mpeg4-picture-timing.js +108 -0
  87. package/services/media/container/pes-header.js +34 -0
  88. package/services/media/playback-declarations.js +57 -0
  89. package/services/media/playback-planner.js +72 -230
  90. package/services/media/probe-packets.js +66 -0
  91. package/services/media/read-playback-declarations.js +18 -0
  92. package/services/media/tracks/AudioTrack.js +9 -0
  93. package/services/media/tracks/TextSubtitleTrack.js +4 -1
  94. package/services/server/OutputLifecycle.js +2 -0
  95. package/services/server/SegmentServing.js +40 -33
  96. package/services/server/ViewerRequests.js +22 -41
  97. package/services/server/controllers/PlaybackController.js +4 -0
  98. package/services/server/controllers/SubtitleController.js +8 -29
  99. package/services/server/transcode-session-files.js +58 -99
  100. package/services/server/wire-outputs.js +58 -15
  101. package/services/storage/IndexMemory.js +85 -0
  102. package/services/storage/MachineBudget.js +27 -3
  103. package/services/storage/allowance.js +8 -1
  104. package/services/storage/piece-store/piece-disk-store.js +18 -3
  105. package/services/storage/piece-store/piece-lru.js +8 -4
  106. package/services/storage/piece-store/shared-piece-store.js +26 -11
  107. package/services/storage/segment-store/SegmentStore.js +49 -18
  108. package/services/storage/wire.js +7 -0
  109. package/services/torrent/Contents.js +52 -20
  110. package/services/torrent/demand/DemandRegister.js +13 -4
  111. package/services/torrent/demand/Window.js +13 -2
  112. package/services/torrent/demand-progress.js +25 -0
  113. package/services/torrent/download/DeadlineScheduler.js +167 -0
  114. package/services/torrent/download/FutureDownload.js +166 -0
  115. package/services/torrent/download/SwarmSelection.js +30 -59
  116. package/services/torrent/download/registry.js +45 -5
  117. package/services/torrent/download/withdraw-claim.js +2 -0
  118. package/services/torrent/torrent-pool.js +39 -600
  119. package/services/torrent/worker/channel.js +8 -1
  120. package/services/torrent/worker/client.js +74 -72
  121. package/services/torrent/worker/files-in-use.js +9 -0
  122. package/services/torrent/worker/handle-state.js +12 -0
  123. package/services/torrent/worker/held-bytes.js +82 -99
  124. package/services/torrent/worker/held-file-bytes.js +17 -0
  125. package/services/torrent/worker/piece-reader.js +67 -683
  126. package/services/torrent/worker/pool-adapter.js +37 -123
  127. package/services/torrent/worker/protocol.js +6 -17
  128. package/services/torrent/worker/whole-file-stats.js +2 -0
  129. package/services/torrent/worker/worker.js +86 -156
  130. package/services/transport/data-channel-handler.js +56 -13
  131. package/services/viewer/DownloadMaps.js +168 -0
  132. package/services/viewer/NativeSourceMap.js +33 -0
  133. package/services/viewer/PriorityMap.js +49 -40
  134. package/services/viewer/PriorityOrchestrator.js +42 -13
  135. package/services/viewer/SourcePreparation.js +278 -0
  136. package/services/viewer/Viewer.js +11 -0
  137. package/services/viewer/Viewers.js +109 -2
  138. package/services/viewer/playback-readiness.js +50 -63
  139. package/services/viewer/segment-demands.js +37 -0
  140. package/services/viewer/urgent-output-ready.js +26 -0
  141. package/services/encode/run-inputs.js +0 -53
  142. package/services/media/keyframe-probe.js +0 -157
  143. package/services/media/media-info-probe.js +0 -93
  144. package/services/torrent/worker/background-fill.js +0 -213
  145. package/services/torrent/worker/resume-warm.js +0 -107
@@ -0,0 +1,45 @@
1
+ /** A declared constant presentation cadence uses picture order as order, not time. */
2
+ export class CadencedPictureOrder {
3
+ #track;
4
+ #index;
5
+ #pending = [];
6
+ #next;
7
+ #last = null;
8
+ #covered = 0;
9
+
10
+ constructor(track, index) {
11
+ this.#track = track;
12
+ this.#index = index;
13
+ this.#next = track.startTimeSeconds;
14
+ }
15
+
16
+ push(packet, order, reset) {
17
+ if (!Number.isSafeInteger(order)) throw new Error("A cadenced picture has no declared order.");
18
+ if (reset) { this.finish(); this.#last = null; }
19
+ const depth = this.#track.reorderDepth;
20
+ if (!Number.isSafeInteger(depth) || depth < 0 || depth > 16) throw new Error("A cadenced picture has no bounded reorder declaration.");
21
+ this.#pending.push({ packet, order, assigned: false });
22
+ if (this.#pending.filter(one => !one.assigned).length > depth + 1) this.#assign();
23
+ }
24
+
25
+ #assign() {
26
+ const one = this.#pending.filter(one => !one.assigned).reduce((first, next) => !first || next.order < first.order ? next : first, null);
27
+ if (!one) return;
28
+ if (this.#last !== null && one.order <= this.#last) throw new Error("Picture order exceeds its declared reorder bound.");
29
+ one.packet.pts = this.#next;
30
+ one.packet.duration = this.#track.presentationCadenceSeconds;
31
+ this.#next += one.packet.duration;
32
+ this.#last = one.order;
33
+ one.assigned = true;
34
+ while (this.#pending[0]?.assigned) {
35
+ this.#index.append(this.#track.trackNumber, this.#pending.shift().packet);
36
+ }
37
+ const through = Math.min(this.#next, ...this.#pending.filter(entry => entry.assigned).map(entry => entry.packet.pts));
38
+ if (through > this.#covered) {
39
+ this.#index.coverThrough(this.#track.trackNumber, through);
40
+ this.#covered = through;
41
+ }
42
+ }
43
+
44
+ finish() { while (this.#pending.length) this.#assign(); }
45
+ }
@@ -4,7 +4,7 @@
4
4
  * A Container knows how to read its own format's track table and index.
5
5
  * Concrete containers (MatroskaContainer, Mp4Container, AviContainer) implement
6
6
  * spec-specific parsing. All byte access goes through `readRange(start,end)` so
7
- * the class works over torrent piece windows.
7
+ * the class reads only bytes already held for the source.
8
8
  *
9
9
  * Spec refs:
10
10
  * - Matroska RFC 9559 §5: EBML, Segment, SeekHead, Tracks, Cues, Clusters
@@ -29,7 +29,7 @@ import { strictReader } from "./unavailable.js";
29
29
 
30
30
  export class Container {
31
31
  /**
32
- * One reader, the one that fetches what is missing, and one container per
32
+ * One strict available-byte reader and one container per
33
33
  * file — the one `ContainerOrchestrator` keeps.
34
34
  *
35
35
  * `readRange` is made strict here: it answers with every byte asked for or
@@ -53,10 +53,11 @@ export class Container {
53
53
  * @param {string} [params.label]
54
54
  * @param {number} [params.portionBytes]
55
55
  */
56
- constructor({ readRange, fileSize, label = "", portionBytes } = {}) {
56
+ constructor({ readRange, fileSize, label = "", portionBytes, packetMemory } = {}) {
57
57
  this.fileSize = fileSize;
58
58
  this.readRange = typeof readRange === "function" ? strictReader(readRange, fileSize) : readRange;
59
59
  this.label = label;
60
+ this.packetMemory = packetMemory;
60
61
  this.portionBytes = Number.isFinite(portionBytes) && portionBytes > 0 ? portionBytes : Number.POSITIVE_INFINITY;
61
62
  }
62
63
 
@@ -360,6 +361,9 @@ export class Container {
360
361
  return "unknown";
361
362
  }
362
363
 
364
+ /** Retained binary packet storage; this excludes declarations and JS heap. */
365
+ packetIndexBytes() { return 0; }
366
+
363
367
  /** Whether `head` (first bytes) looks like this container. */
364
368
  static detect(_head) {
365
369
  return false;
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * @file Container factory — detects format and returns the precise Container subclass.
3
3
  *
4
- * Sniffs first 16 bytes (same as container-index/index.js) and instantiates
5
- * MatroskaContainer / Mp4Container / AviContainer. Falls back to null (unknown).
4
+ * Reads the first 16 bytes and selects Matroska, MP4, AVI, TS, PS or ASF.
5
+ * Other formats use the supplied packet probe; without a probe they return null.
6
6
  * Orchestrators depend on this, not on concrete constructors.
7
7
  */
8
8
 
9
9
  import { MatroskaContainer } from "./MatroskaContainer.js";
10
10
  import { Mp4Container } from "./Mp4Container.js";
11
11
  import { AviContainer } from "./AviContainer.js";
12
+ import { MpegTsContainer } from "./MpegTsContainer.js";
13
+ import { AsfContainer } from "./AsfContainer.js";
14
+ import { MpegPsContainer } from "./MpegPsContainer.js";
12
15
  import { strictReader } from "./unavailable.js";
16
+ import { FfprobeContainer } from "./FfprobeContainer.js";
13
17
 
14
18
  const SNIFF_BYTES = 16;
15
19
 
@@ -33,6 +37,9 @@ export class ContainerFactory {
33
37
  if (MatroskaContainer.detect(head)) return new MatroskaContainer(params);
34
38
  if (Mp4Container.detect(head)) return new Mp4Container(params);
35
39
  if (AviContainer.detect(head)) return new AviContainer(params);
36
- return null;
40
+ if (MpegTsContainer.detect(head)) return new MpegTsContainer(params);
41
+ if (AsfContainer.detect(head)) return new AsfContainer(params);
42
+ if (MpegPsContainer.detect(head)) return new MpegPsContainer(params);
43
+ return typeof params.probe === "function" ? new FfprobeContainer(params) : null;
37
44
  }
38
45
  }
@@ -0,0 +1,222 @@
1
+ import { Container } from "./Container.js";
2
+ import { PacketIndex } from "./PacketIndex.js";
3
+ import { PacketRecords } from "./PacketRecords.js";
4
+ import { BytesUnavailable } from "./unavailable.js";
5
+ import { IndexMemoryUnavailable } from "./memory-unavailable.js";
6
+ import { ffprobeExtradata } from "./ffprobe-record.js";
7
+ import { VideoTrack } from "../tracks/VideoTrack.js";
8
+ import { AudioTrack } from "../tracks/AudioTrack.js";
9
+ import { SubtitleTrack } from "../tracks/SubtitleTrack.js";
10
+ import { TextSubtitleTrack } from "../tracks/TextSubtitleTrack.js";
11
+ import { MatroskaContainer } from "./MatroskaContainer.js";
12
+ import { Mp4Container } from "./Mp4Container.js";
13
+ import { flvPacketRanges } from "./flv-packet-ranges.js";
14
+
15
+ const TEXT_CODECS = new Map([
16
+ ["subrip", "S_TEXT/UTF8"], ["srt", "S_TEXT/UTF8"],
17
+ ["ass", "S_TEXT/ASS"], ["ssa", "S_TEXT/SSA"],
18
+ ["webvtt", "S_TEXT/WEBVTT"], ["vtt", "S_TEXT/WEBVTT"],
19
+ ["mov_text", "tx3g"], ["text", "S_TEXT/UTF8"]
20
+ ]);
21
+
22
+ const indexed = track => track.type === "video" || track.type === "audio" ||
23
+ (track.type === "subtitle" && track.isTextBased());
24
+
25
+ /** Additional formats use the same strict byte reader through an injected demuxer. */
26
+ export class FfprobeContainer extends Container {
27
+ #probe;
28
+ #tracks;
29
+ #info;
30
+ #index;
31
+ #records;
32
+ #complete = false;
33
+ #packetScanComplete = false;
34
+ #indexedRecords = 0;
35
+ #timings = new Map();
36
+
37
+ constructor(params) {
38
+ super(params);
39
+ this.#probe = params.probe;
40
+ this.#records = new PacketRecords(this.packetMemory);
41
+ }
42
+ get formatName() { return "ffprobe"; }
43
+ packetIndexBytes() { return this.#index?.allocatedBytes() ?? 0; }
44
+
45
+ async #read(statement, onRecord) {
46
+ const result = await this.#probe(statement, onRecord);
47
+ if (result.kind === "needs-memory") throw new IndexMemoryUnavailable(result.bytes);
48
+ if (result.kind === "needs-ranges") {
49
+ const [start, end] = result.ranges[0];
50
+ throw new BytesUnavailable(start, end, 0);
51
+ }
52
+ if (result.kind !== "result") throw new Error(result.message ?? result.reason ?? "Packet probe was cancelled.");
53
+ return result.value;
54
+ }
55
+
56
+ async readTracks() {
57
+ if (this.#tracks) return this.#tracks;
58
+ const records = await this.#read("streams");
59
+ const counts = { video: 0, audio: 0, subtitle: 0 };
60
+ const tracks = [];
61
+ for (const record of records) {
62
+ if (record.kind !== "stream" || !Object.hasOwn(counts, record.codec_type)) continue;
63
+ const stream = integer(record.index, "stream index");
64
+ if (tracks.some(track => track.trackNumber === stream + 1)) throw new Error("Duplicate ffprobe stream.");
65
+ const privateSize = record.extradata_size === undefined ? 0 : integer(record.extradata_size, "extradata size");
66
+ const base = { trackNumber: stream + 1, declaredIndex: counts[record.codec_type]++, codecId: record.codec_name,
67
+ language: record["tag:language"] ?? "", declaresLanguage: Object.hasOwn(record, "tag:language"),
68
+ name: record["tag:title"] ?? "", isEnabled: true, isDefault: record["disposition:default"] === "1",
69
+ declaresDefault: Object.hasOwn(record, "disposition:default"),
70
+ codecPrivateB64: privateSize ? ffprobeExtradata(record.extradata, privateSize).toString("base64") : "" };
71
+ if (base.codecId === "alac" && privateSize) {
72
+ const atom = Buffer.from(base.codecPrivateB64, "base64");
73
+ if (atom.length < 36 || atom.readUInt32BE() !== atom.length || atom.toString("ascii", 4, 8) !== "alac") {
74
+ throw new Error("The ALAC probe did not supply a complete decoder atom.");
75
+ }
76
+ base.codecPrivateB64 = atom.subarray(12).toString("base64");
77
+ }
78
+ let track;
79
+ if (record.codec_type === "video") {
80
+ track = new VideoTrack({ ...base, width: number(record.width), height: number(record.height), fps: ratio(record.r_frame_rate) });
81
+ track.reorderDepth = record.has_b_frames === undefined ? 0 : integer(record.has_b_frames, "reorder depth");
82
+ } else if (record.codec_type === "audio") {
83
+ track = new AudioTrack({ ...base, channels: number(record.channels), samplingFrequency: number(record.sample_rate),
84
+ bitDepth: Number(/^pcm_[suf](\d+)/.exec(record.codec_name)?.[1]) ||
85
+ number(record.bits_per_raw_sample) || number(record.bits_per_sample) || null });
86
+ track.prerollSeconds = track.codecId === "opus" ? 0.08 : 0;
87
+ if (track.codecId === "opus") {
88
+ const head = Buffer.from(track.codecPrivateB64, "base64");
89
+ if (head.length < 19 || head.toString("ascii", 0, 8) !== "OpusHead") throw new Error("Opus decoder configuration is absent.");
90
+ track.codecDelaySeconds = head.readUInt16LE(10) / 48000;
91
+ track.seekPrerollSeconds = 0.08;
92
+ }
93
+ } else {
94
+ const codecId = TEXT_CODECS.get(record.codec_name);
95
+ const Type = codecId ? TextSubtitleTrack : SubtitleTrack;
96
+ track = new Type({ ...base, ...(codecId ? { codecId } : {}), isForced: record["disposition:forced"] === "1" });
97
+ }
98
+ tracks.push(track);
99
+ }
100
+ if (!tracks.length) throw new Error("The packet probe found no declared media streams.");
101
+ const format = records.find(record => record.kind === "format");
102
+ this.#info = { format: format?.format_name ?? "unknown", durationSeconds: number(format?.duration), startTimeSeconds: number(format?.start_time) };
103
+ this.#tracks = tracks;
104
+ return tracks;
105
+ }
106
+
107
+ async readMediaInfo() { await this.readTracks(); return this.#info; }
108
+
109
+ async readPacketIndex(interval) {
110
+ const tracks = await this.readTracks();
111
+ if (this.#complete) return this.#index;
112
+ let position = 0;
113
+ let missing;
114
+ try { if (!this.#packetScanComplete) await this.#read("packets", record => {
115
+ if (record.kind === "scan-start") { position = 0; return; }
116
+ if (record.kind === "scan-end") {
117
+ if (position !== this.#records.length) throw new Error("Packet scan ended before its retained index.");
118
+ this.#packetScanComplete = true;
119
+ return;
120
+ }
121
+ if (record.kind !== "packet") return;
122
+ const streamId = integer(record.stream_index, "packet stream") + 1;
123
+ const track = tracks.find(track => track.trackNumber === streamId);
124
+ if (!track || !indexed(track)) return;
125
+ const start = integer(record.pos, "packet address"), size = integer(record.size, "packet size");
126
+ if (!size || !Number.isSafeInteger(start + size) || start + size > this.fileSize) throw new Error("Packet bytes are outside the source file.");
127
+ const pts = number(record.pts_time), duration = number(record.duration_time);
128
+ const dts = number(record.dts_time) ?? (track.type === "subtitle" ? pts : null);
129
+ if (pts === null || dts === null || duration === null || duration < 0 ||
130
+ (duration === 0 && track.type !== "subtitle")) throw new Error("The demuxer cannot state complete packet timing.");
131
+ const expectedHash = /^SHA256:([a-fA-F0-9]{64})$/.exec(record.data_hash ?? "")?.[1]?.toLowerCase();
132
+ if (!expectedHash) throw new Error("The demuxer cannot verify packet byte addresses.");
133
+ const packet = { pts, dts, duration, keyframe: record.flags?.includes("K") === true,
134
+ ranges: [[start, start + size - 1]], expectedHash, streamId };
135
+ const previous = this.#records.at(position++);
136
+ if (previous) {
137
+ if (previous.pts !== pts || previous.dts !== dts || previous.duration !== duration ||
138
+ previous.keyframe !== packet.keyframe || previous.streamId !== streamId || previous.expectedHash !== expectedHash ||
139
+ previous.ranges[0][0] !== start || previous.ranges[0][1] !== start + size - 1) {
140
+ throw new Error("Repeated packet scan changed a previously indexed packet.");
141
+ }
142
+ } else this.#records.push(packet);
143
+ }); } catch (error) {
144
+ if (!(error instanceof BytesUnavailable)) throw error;
145
+ missing = error;
146
+ }
147
+ if (!missing && !this.#packetScanComplete) throw new Error("Packet scan did not report its completion.");
148
+ const index = this.#index ?? new PacketIndex({ packetMemory: this.packetMemory });
149
+ const timings = this.#timings;
150
+ if (!this.#index) {
151
+ for (const track of tracks) {
152
+ if (!indexed(track)) continue;
153
+ index.declareTrack(track.trackNumber, { type: track.type, codecId: track.codecId, reorderDepth: track.reorderDepth ?? 0, prerollSeconds: track.prerollSeconds ?? 0 });
154
+ }
155
+ this.#index = index;
156
+ }
157
+ while (this.#indexedRecords < this.#records.length) {
158
+ const record = this.#records.at(this.#indexedRecords);
159
+ const id = record.streamId;
160
+ const track = tracks.find(track => track.trackNumber === id);
161
+ const start = record.ranges[0][0], size = record.ranges[0][1] - start + 1;
162
+ const { pts, dts, duration } = record;
163
+ const ranges = this.#info.format.split(",").includes("flv")
164
+ ? await flvPacketRanges({ readRange: this.readRange, fileSize: this.fileSize, position: start, size, track, pts, dts })
165
+ : [[start, start + size - 1]];
166
+ index.append(id, { pts, dts, duration, keyframe: record.keyframe,
167
+ ranges, expectedHash: record.expectedHash });
168
+ const timing = timings.get(id) ?? { count: 0, tail: [] };
169
+ timing.count++;
170
+ timing.tail.push({ pts, duration });
171
+ const keep = track.type === "video" ? track.reorderDepth + 1 : 1;
172
+ if (timing.tail.length > keep) timing.tail.shift();
173
+ timings.set(id, timing);
174
+ this.#indexedRecords++;
175
+ }
176
+ for (const track of tracks) {
177
+ if (!indexed(track)) continue;
178
+ if (!missing) index.complete(track.trackNumber);
179
+ else {
180
+ const timing = timings.get(track.trackNumber) ?? { count: 0, tail: [] };
181
+ const tail = timing.tail;
182
+ if (tail.length && (track.type !== "video" || timing.count > track.reorderDepth)) {
183
+ const through = track.type === "video" ? Math.min(...tail.map(packet => packet.pts)) : tail[0].pts + tail[0].duration;
184
+ index.coverThrough(track.trackNumber, Math.max(0, through));
185
+ }
186
+ }
187
+ }
188
+ if (missing) {
189
+ if (!interval || tracks.filter(track => indexed(track) &&
190
+ (!interval.trackIds || interval.trackIds.includes(track.trackNumber))).some(track =>
191
+ index.inputFor({ trackId: track.trackNumber, from: interval.from, to: interval.to }).kind !== "result")) throw missing;
192
+ return index;
193
+ }
194
+ this.#index = index;
195
+ this.#complete = true;
196
+ this.#records.dispose();
197
+ return index;
198
+ }
199
+
200
+ async parseKeyframeIndex() {
201
+ const tracks = await this.readTracks();
202
+ const video = tracks.find(track => track.type === "video");
203
+ if (!video) return null;
204
+ return { times: (await this.readPacketIndex()).keyframesOf(video.trackNumber), tolerance: 0 };
205
+ }
206
+
207
+ static cueTextOf(payload, codecId) {
208
+ if (codecId === "tx3g") return Mp4Container.cueTextOf(payload, codecId);
209
+ return MatroskaContainer.cueTextOf(payload, codecId);
210
+ }
211
+ }
212
+
213
+ function number(value) { return value !== undefined && value !== "N/A" && Number.isFinite(Number(value)) ? Number(value) : null; }
214
+ function integer(value, field) {
215
+ const result = number(value);
216
+ if (!Number.isSafeInteger(result) || result < 0) throw new Error(`The demuxer cannot state ${field}.`);
217
+ return result;
218
+ }
219
+ function ratio(value) {
220
+ const [numerator, denominator] = String(value ?? "").split("/").map(Number);
221
+ return numerator > 0 && denominator > 0 ? numerator / denominator : null;
222
+ }
@@ -34,16 +34,17 @@
34
34
 
35
35
  import { Container } from "./Container.js";
36
36
  import { VideoTrack } from "../tracks/VideoTrack.js";
37
+ import { h264Configuration } from "./h264-configuration.js";
38
+ import { hevcConfiguration } from "./hevc-configuration.js";
37
39
  import { AudioTrack } from "../tracks/AudioTrack.js";
38
40
  import { TextSubtitleTrack, TEXT_CODECS_MATROSKA } from "../tracks/TextSubtitleTrack.js";
39
41
  import { ImageSubtitleTrack } from "../tracks/ImageSubtitleTrack.js";
40
42
  import { ContainerTrack } from "../tracks/ContainerTrack.js";
41
- import { iterateElements, readFloat, readUint, readVint } from "./ebml-reader.js";
42
- import { ElementReader, sizeIsUnknown } from "./ebml-stream.js";
43
+ import { iterateElements, readFloat, readUint } from "./ebml-reader.js";
44
+ import { ElementReader } from "./ebml-stream.js";
43
45
  import { firstFrameOffset, readBlockHeader, walkHeldClusters } from "./matroska-clusters.js";
44
-
45
- /** Enough to hold the EBML header and the Segment's own header. */
46
- const HEAD_BYTES = 64 * 1024;
46
+ import { readMatroskaPackets } from "./matroska-packets.js";
47
+ import { RetainedReads } from "./RetainedReads.js";
47
48
 
48
49
  const ID_EBML = 0x1a45dfa3;
49
50
  const ID_SEGMENT = 0x18538067;
@@ -91,6 +92,7 @@ const ID_DISPLAY_WIDTH = 0x54b0;
91
92
  const ID_DISPLAY_HEIGHT = 0x54ba;
92
93
  const ID_SAMPLING_FREQUENCY = 0xb5;
93
94
  const ID_CHANNELS = 0x9f;
95
+ const ID_AUDIO_BIT_DEPTH = 0x6264;
94
96
 
95
97
  /** RFC 9559 §5.1.2.1: nanoseconds per tick when Info omits TimestampScale. */
96
98
  const DEFAULT_TIMESTAMP_SCALE = 1_000_000;
@@ -127,13 +129,15 @@ function readString(buf, el) {
127
129
 
128
130
  export class MatroskaContainer extends Container {
129
131
  /** @type {Buffer | null} */
130
- #head = null;
132
+ #declarations = null;
131
133
 
132
134
  /** @type {SegmentLayout | null} */
133
135
  #layout = null;
134
136
 
135
137
  /** @type {Array<import("../tracks/ContainerTrack.js").ContainerTrack> | null} */
136
138
  #tracks = null;
139
+ #packets = null;
140
+ #packetState = {};
137
141
 
138
142
  /**
139
143
  * The Cues reading: undefined until read, null when the file has none.
@@ -145,6 +149,11 @@ export class MatroskaContainer extends Container {
145
149
  return "matroska";
146
150
  }
147
151
 
152
+ packetIndexBytes() {
153
+ return (this.#packets?.allocatedBytes() ?? 0) +
154
+ [...(this.#packetState.packets?.values() ?? [])].reduce((sum, records) => sum + records.allocatedBytes, 0);
155
+ }
156
+
148
157
  static detect(head) {
149
158
  return isMatroska(head);
150
159
  }
@@ -218,14 +227,6 @@ export class MatroskaContainer extends Container {
218
227
  });
219
228
  }
220
229
 
221
- /** @returns {Promise<Buffer>} */
222
- async #headBytes() {
223
- if (this.#head === null) {
224
- this.#head = await this.readRange(0, Math.min(HEAD_BYTES, this.fileSize) - 1);
225
- }
226
- return this.#head;
227
- }
228
-
229
230
  /**
230
231
  * Where the Segment's top-level elements are.
231
232
  *
@@ -242,18 +243,22 @@ export class MatroskaContainer extends Container {
242
243
  if (this.#layout !== null) {
243
244
  return this.#layout;
244
245
  }
245
- const head = await this.#headBytes();
246
- if (!isMatroska(head)) {
247
- return null;
246
+ const reader = new ElementReader({ read: this.readRange, fileSize: this.fileSize, portionBytes: this.portionBytes });
247
+ let segment = null;
248
+ for (let at = 0; at < this.fileSize;) {
249
+ const header = await reader.header(at, this.fileSize);
250
+ if (!header || (at === 0 && header.id !== ID_EBML)) return null;
251
+ if (header.end !== null && header.end > this.fileSize) throw new Error("Matroska top-level element exceeds the file.");
252
+ if (header.id === ID_SEGMENT) { segment = header; break; }
253
+ if (header.end === null || header.end <= at) throw new Error("Matroska top-level element has no bounded next position.");
254
+ at = header.end;
248
255
  }
249
- const segment = segmentHeaderIn(head);
250
256
  if (!segment) {
251
257
  return null;
252
258
  }
253
259
  const segmentDataOffset = segment.dataOffset;
254
260
  const segmentEnd = segment.size === null ? this.fileSize : Math.min(this.fileSize, segmentDataOffset + segment.size);
255
261
 
256
- const reader = new ElementReader({ read: this.readRange, fileSize: this.fileSize, portionBytes: this.portionBytes });
257
262
  /** @type {Map<number, number>} */
258
263
  const found = new Map();
259
264
  /** @type {Map<number, number>} */
@@ -367,6 +372,7 @@ export class MatroskaContainer extends Container {
367
372
  * @returns {Promise<import("../tracks/index.js").ContainerTrack[]>}
368
373
  */
369
374
  async readTracks() {
375
+ this.#declarations ??= new RetainedReads(this.packetMemory);
370
376
  if (this.#tracks !== null) {
371
377
  return this.#tracks;
372
378
  }
@@ -392,7 +398,8 @@ export class MatroskaContainer extends Container {
392
398
  break;
393
399
  }
394
400
  if (entry.id === ID_TRACK_ENTRY) {
395
- const data = await within.data(entry);
401
+ const data = entry.size === 0 ? Buffer.alloc(0) : entry.size > this.portionBytes ? null
402
+ : await this.#declarations.read(entry.dataOffset, entry.end - 1, () => within.data(entry));
396
403
  // An entry larger than one portion is refused rather than assembled.
397
404
  // Its type cannot be known without its data, so it is kept as a track
398
405
  // of no kind — which makes the count of its kind short by one, and
@@ -401,6 +408,20 @@ export class MatroskaContainer extends Container {
401
408
  const fields = data ? parseTrackEntry(data) : { refused: true, trackNumber: null };
402
409
  const track = trackFrom(fields, counters);
403
410
  if (track) {
411
+ if (["V_MPEG4/ISO/AVC", "V_MPEGH/ISO/HEVC"].includes(track.codecId) && track.codecPrivateB64) {
412
+ const configuration = (track.codecId === "V_MPEG4/ISO/AVC" ? h264Configuration : hevcConfiguration)(Buffer.from(track.codecPrivateB64, "base64"));
413
+ track.codecConfiguration = configuration;
414
+ track.reorderDepth = configuration.reorderDepth;
415
+ track.width = configuration.width;
416
+ track.height = configuration.height;
417
+ track.bitDepth = configuration.bitDepth;
418
+ track.fps = configuration.fps ?? track.fps;
419
+ }
420
+ track.defaultDurationSeconds = fields.defaultDurationSeconds || track.defaultDurationSeconds || 0;
421
+ track.seekPrerollSeconds = Math.max(fields.seekPrerollSeconds ?? 0, track.seekPrerollSeconds ?? 0);
422
+ track.codecDelaySeconds = fields.codecDelaySeconds ?? 0;
423
+ track.timestampScale = fields.timestampScale ?? 1;
424
+ track.codecRanges = [[entry.dataOffset, entry.end - 1]];
404
425
  result.push(track);
405
426
  }
406
427
  }
@@ -410,6 +431,22 @@ export class MatroskaContainer extends Container {
410
431
  return result;
411
432
  }
412
433
 
434
+ async readPacketIndex(interval) {
435
+ if (this.#packets) return this.#packets;
436
+ const layout = await this.#segmentLayout();
437
+ if (!layout) throw new Error("Matroska Segment is absent.");
438
+ const tracks = await this.readTracks();
439
+ const info = await this.readMediaInfo();
440
+ const index = await readMatroskaPackets({ readRange: this.readRange,
441
+ fileSize: this.fileSize, portionBytes: this.portionBytes, layout, tracks,
442
+ durationSeconds: info.durationSeconds, interval, state: this.#packetState, packetMemory: this.packetMemory });
443
+ if (index.isComplete()) {
444
+ this.#packets = index;
445
+ this.#packetState = {};
446
+ }
447
+ return index;
448
+ }
449
+
413
450
  /**
414
451
  * The Cues table, read once, every CuePoint with every track it names.
415
452
  *
@@ -600,7 +637,13 @@ export class MatroskaContainer extends Container {
600
637
  */
601
638
  static cueTextOf(payload, codecId) {
602
639
  const text = Buffer.isBuffer(payload) ? payload.toString("utf8") : String(payload ?? "");
603
- if (codecId !== "S_TEXT/ASS" && codecId !== "S_TEXT/SSA") {
640
+ if (String(codecId).startsWith("D_WEBVTT/")) {
641
+ // WebM blocks prefix the cue with its identifier and settings lines.
642
+ const first = text.indexOf("\n"), second = text.indexOf("\n", first + 1);
643
+ if (first < 0 || second < 0) throw new Error("WebM subtitle cue prefixes are truncated.");
644
+ return text.slice(second + 1);
645
+ }
646
+ if (!["S_TEXT/ASS", "S_TEXT/SSA", "S_ASS", "S_SSA"].includes(codecId)) {
604
647
  return text;
605
648
  }
606
649
  const fields = text.split(",");
@@ -658,30 +701,6 @@ function isMatroska(head) {
658
701
  return head.length >= 4 && head.readUInt32BE(0) === ID_EBML;
659
702
  }
660
703
 
661
- /**
662
- * The Segment element's header, from the top-level elements of the head.
663
- *
664
- * @param {Buffer} head
665
- * @returns {{ dataOffset: number, size: number | null } | null} `size` is null
666
- * for an unknown-sized Segment (RFC 8794 §6.2), which runs to the file's end.
667
- */
668
- function segmentHeaderIn(head) {
669
- let offset = 0;
670
- while (offset < head.length) {
671
- const id = readVint(head, offset, true);
672
- const size = id && readVint(head, offset + id.length, false);
673
- if (!id || !size) {
674
- return null;
675
- }
676
- const dataOffset = offset + id.length + size.length;
677
- if (id.value === ID_SEGMENT) {
678
- return { dataOffset, size: sizeIsUnknown(size) ? null : size.value };
679
- }
680
- offset = dataOffset + size.value;
681
- }
682
- return null;
683
- }
684
-
685
704
  /**
686
705
  * The SeekHead's entries, id to position relative to the Segment's data.
687
706
  *
@@ -818,6 +837,10 @@ function parseTrackEntry(data) {
818
837
  };
819
838
  for (const f of iterateElements(data)) {
820
839
  switch (f.id) {
840
+ case 0x23e383: fields.defaultDurationSeconds = readUint(data, f.dataOffset, f.size) / 1e9; break;
841
+ case 0x56bb: fields.seekPrerollSeconds = readUint(data, f.dataOffset, f.size) / 1e9; break;
842
+ case 0x56aa: fields.codecDelaySeconds = readUint(data, f.dataOffset, f.size) / 1e9; break;
843
+ case 0x23314f: fields.timestampScale = readFloat(data, f.dataOffset, f.size); break;
821
844
  case ID_TRACK_NUMBER: fields.trackNumber = readUint(data, f.dataOffset, f.size); break;
822
845
  case ID_TRACK_TYPE: fields.type = readUint(data, f.dataOffset, f.size); break;
823
846
  case ID_CODEC_ID: fields.codecId = readString(data, f); break;
@@ -853,6 +876,8 @@ function parseTrackEntry(data) {
853
876
  fields.samplingFreq = af.size === 8 ? data.readDoubleBE(af.dataOffset) : readFloat(data, af.dataOffset, af.size);
854
877
  } else if (af.id === ID_CHANNELS) {
855
878
  fields.channels = readUint(data, af.dataOffset, af.size);
879
+ } else if (af.id === ID_AUDIO_BIT_DEPTH) {
880
+ fields.bitDepth = readUint(data, af.dataOffset, af.size);
856
881
  }
857
882
  }
858
883
  break;
@@ -916,7 +941,8 @@ function trackFrom(fields, counters) {
916
941
  isCommentary: fields.isCommentary,
917
942
  isVisualImpaired: fields.isVisual,
918
943
  channels: fields.channels,
919
- samplingFrequency: fields.samplingFreq
944
+ samplingFrequency: fields.samplingFreq,
945
+ bitDepth: fields.bitDepth
920
946
  });
921
947
  }
922
948
  if (fields.type === TRACK_TYPE_SUBTITLE) {