@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,196 @@
1
+ import { mpegAudioFrame } from "./mpeg-audio-frame.js";
2
+ import { PacketIndex } from "./PacketIndex.js";
3
+ import { H264ElementaryIndex } from "./h264-elementary-index.js";
4
+ import { HevcElementaryIndex } from "./hevc-elementary-index.js";
5
+ import { LatmFrames } from "./latm-frame.js";
6
+ import { ac3Frame } from "./ac3-frame.js";
7
+ import { dtsFrame } from "./dts-frame.js";
8
+ import { MpegAudioReservoir } from "./mpeg-audio-reservoir.js";
9
+
10
+ /** Preserve elementary-frame addresses while transport packets are discarded. */
11
+ export class MpegElementaryIndex {
12
+ index;
13
+ #states = new Map();
14
+ #completed = false;
15
+
16
+ constructor(tracks, { packetMemory, index = null } = {}) {
17
+ this.index = index ?? new PacketIndex({ packetMemory, deferMemory: true });
18
+ for (const track of tracks.filter(track => ["video", "audio"].includes(track.type))) {
19
+ if (!index) this.index.declareTrack(track.trackNumber, { type: track.type, codecId: track.codecId });
20
+ const supported = track.type === "video" ? ["mpeg1video", "mpeg2video", "h264", "hevc"].includes(track.codecId)
21
+ : ["mp1", "mp2", "mp3", "aac", "aac_latm", "ac3", "eac3", "dts"].includes(track.codecId);
22
+ if (!supported) {
23
+ this.index.refuse(track.trackNumber, `elementary-index-unavailable:${track.codecId || "unknown"}`);
24
+ continue;
25
+ }
26
+ this.#states.set(track.trackNumber, { track, offset: 0, mappings: [], stamps: [], carry: Buffer.alloc(0),
27
+ latm: track.codecId === "aac_latm" ? new LatmFrames() : null,
28
+ nalIndex: track.codecId === "h264" ? new H264ElementaryIndex(track, this.index)
29
+ : track.codecId === "hevc" ? new HevcElementaryIndex(track, this.index) : null,
30
+ picture: null, pendingStart: 0, lastPicture: -1, presentationBase: null, lastDts: null, referencePts: null,
31
+ audioBytes: Buffer.alloc(0), audioStart: 0, audioPts: null, lastTimestamp: null, clockShift: 0,
32
+ reservoir: new MpegAudioReservoir(), packetCount: 0,
33
+ lastExtension: -1, requiresStamp: false });
34
+ }
35
+ }
36
+
37
+ push(id, bytes, sourceStart, { pts = null, dts = null } = {}) {
38
+ const state = this.#states.get(id);
39
+ if (!state || bytes.length === 0) return;
40
+ const position = state.offset;
41
+ if (!state.nalIndex) state.mappings.push({ from: position, to: position + bytes.length, sourceStart });
42
+ let normalized = null;
43
+ if (pts !== null) {
44
+ const cycle = 2 ** 33 / 90000;
45
+ const clock = dts ?? pts;
46
+ if (state.lastTimestamp === null) {
47
+ if (clock > pts + cycle / 2) state.clockShift -= cycle;
48
+ else if (clock < pts - cycle / 2) state.clockShift += cycle;
49
+ } else {
50
+ if (clock + state.clockShift < state.lastTimestamp - cycle / 2) state.clockShift += cycle;
51
+ else if (clock + state.clockShift > state.lastTimestamp + cycle / 2) state.clockShift -= cycle;
52
+ }
53
+ state.lastTimestamp = clock + state.clockShift;
54
+ let presentation = pts + state.clockShift;
55
+ if (presentation < state.lastTimestamp - cycle / 2) presentation += cycle;
56
+ else if (presentation > state.lastTimestamp + cycle / 2) presentation -= cycle;
57
+ normalized = { position, pts: presentation, dts: state.lastTimestamp };
58
+ if (!state.nalIndex) state.stamps.push(normalized);
59
+ }
60
+ state.offset += bytes.length;
61
+ if (state.nalIndex) state.nalIndex.push(bytes, sourceStart, normalized);
62
+ else if (state.track.type === "video") this.#video(state, bytes, position);
63
+ else this.#audio(state, bytes);
64
+ }
65
+
66
+ complete() {
67
+ if (this.#completed) return this.index;
68
+ for (const state of this.#states.values()) {
69
+ if (state.nalIndex) { state.nalIndex.complete(); continue; }
70
+ if (state.track.type === "video" && state.picture) this.#closePicture(state, state.offset);
71
+ if (state.track.type === "audio" && state.audioBytes.length) throw new Error("MPEG audio ends inside a frame.");
72
+ this.index.complete(state.track.trackNumber);
73
+ }
74
+ this.#completed = true;
75
+ return this.index;
76
+ }
77
+
78
+ #video(state, chunk, position) {
79
+ const bytes = Buffer.concat([state.carry, chunk]);
80
+ const base = position - state.carry.length;
81
+ for (let at = 0; at + 3 < bytes.length; at++) {
82
+ if (bytes[at] !== 0 || bytes[at + 1] !== 0 || bytes[at + 2] !== 1) continue;
83
+ const offset = base + at, code = bytes[at + 3];
84
+ if (code === 0xb5 && at + 9 <= bytes.length && bytes[at + 4] >> 4 === 8 &&
85
+ state.picture && offset > state.lastExtension) {
86
+ if ((bytes[at + 6] & 3) !== 3) throw new Error("MPEG field pictures require field-specific packet timing.");
87
+ const repeat = (bytes[at + 7] & 2) !== 0;
88
+ const top = (bytes[at + 7] & 128) !== 0;
89
+ const progressiveFrame = (bytes[at + 8] & 128) !== 0;
90
+ if (repeat) {
91
+ if (!progressiveFrame) throw new Error("MPEG repeated fields require field-specific packet timing.");
92
+ state.picture.duration *= state.track.progressiveSequence ? top ? 3 : 2 : 1.5;
93
+ state.requiresStamp = true;
94
+ }
95
+ state.lastExtension = offset;
96
+ }
97
+ if ((code === 0xb3 || code === 0xb8) && offset > state.lastPicture) {
98
+ if (state.pendingStart === null) state.pendingStart = offset;
99
+ }
100
+ if (code !== 0 || at + 6 > bytes.length || offset <= state.lastPicture) continue;
101
+ if (!(state.track.fps > 0)) throw new Error("MPEG video cadence is not declared.");
102
+ const reference = (bytes[at + 4] << 2) | (bytes[at + 5] >> 6);
103
+ const type = (bytes[at + 5] >> 3) & 7;
104
+ if (type < 1 || type > 3) throw new Error("MPEG picture type is invalid.");
105
+ const start = state.pendingStart ?? offset;
106
+ if (state.picture) this.#closePicture(state, start);
107
+ const stamp = stampAt(state, offset);
108
+ if (state.requiresStamp && !stamp) throw new Error("MPEG repeated frames require an explicit following timestamp.");
109
+ state.requiresStamp = false;
110
+ if (stamp) state.presentationBase = stamp.pts - reference / state.track.fps;
111
+ if (state.presentationBase === null) throw new Error("MPEG picture has no presentation timestamp.");
112
+ const pts = stamp?.pts ?? state.presentationBase + reference / state.track.fps;
113
+ const dts = stamp?.dts ?? (state.lastDts === null ? pts : state.lastDts + 1 / state.track.fps);
114
+ state.picture = { start, pts, dts, duration: 1 / state.track.fps, keyframe: type === 1 };
115
+ state.lastDts = dts;
116
+ state.lastPicture = offset;
117
+ state.pendingStart = null;
118
+ if (type !== 3) {
119
+ // The next reference picture proves that the preceding reference and
120
+ // its intervening B pictures have all been read in decode order.
121
+ if (state.referencePts !== null && pts > state.referencePts) this.index.coverThrough(state.track.trackNumber, Math.max(0, state.referencePts));
122
+ state.referencePts = pts;
123
+ }
124
+ }
125
+ state.carry = Buffer.from(bytes.subarray(Math.max(0, bytes.length - 9)));
126
+ }
127
+
128
+ #closePicture(state, end) {
129
+ const { start, ...packet } = state.picture;
130
+ if (!(end > start)) throw new Error("MPEG picture has no bytes.");
131
+ this.index.append(state.track.trackNumber, { ...packet, ranges: addresses(state, start, end) });
132
+ state.mappings = state.mappings.filter(mapping => mapping.to > end);
133
+ }
134
+
135
+ #audio(state, chunk) {
136
+ state.audioBytes = Buffer.concat([state.audioBytes, chunk]);
137
+ while (state.audioBytes.length >= (state.track.codecId === "dts" ? 24 : ["aac", "ac3", "eac3"].includes(state.track.codecId) ? 7 : 4)) {
138
+ const frame = state.latm ? state.latm.read(state.audioBytes) : state.track.codecId === "aac" ? adtsFrame(state.audioBytes)
139
+ : ["ac3", "eac3"].includes(state.track.codecId) ? ac3Frame(state.audioBytes)
140
+ : state.track.codecId === "dts" ? dtsFrame(state.audioBytes) : mpegAudioFrame(state.audioBytes);
141
+ if (state.audioBytes.length < frame.size) break;
142
+ const stamp = stampAt(state, state.audioStart);
143
+ if (stamp) state.audioPts = stamp.pts;
144
+ if (state.audioPts === null) throw new Error("MPEG audio frame has no presentation timestamp.");
145
+ if (frame.codecPrivateB64) {
146
+ if (state.track.codecPrivateB64 && state.track.codecPrivateB64 !== frame.codecPrivateB64) throw new Error("AAC configuration changes require packet-specific declarations.");
147
+ state.track.codecPrivateB64 = frame.codecPrivateB64;
148
+ }
149
+ Object.assign(state.track, { codecId: frame.codecId, samplingFrequency: frame.sampleRate, channels: frame.channels });
150
+ const reservoir = frame.codecId === "mp3" ? state.reservoir.prepare(state.audioBytes, frame.size, state.packetCount) : null;
151
+ this.index.append(state.track.trackNumber, { pts: state.audioPts, dts: state.audioPts,
152
+ ...(reservoir ? { decodeFromIndex: reservoir.decodeFromIndex } : {}),
153
+ ...(frame.bitOffset === undefined ? {} : { bitOffset: frame.bitOffset, bitLength: frame.bitLength }),
154
+ duration: frame.duration, keyframe: true, ranges: addresses(state, state.audioStart + (frame.headerBytes ?? 0), state.audioStart + frame.size) });
155
+ reservoir?.commit();
156
+ state.packetCount++;
157
+ state.audioPts += frame.duration;
158
+ this.index.coverThrough(state.track.trackNumber, Math.max(0, state.audioPts));
159
+ state.audioStart += frame.size;
160
+ state.audioBytes = state.audioBytes.subarray(frame.size);
161
+ state.mappings = state.mappings.filter(mapping => mapping.to > state.audioStart);
162
+ }
163
+ state.audioBytes = Buffer.from(state.audioBytes);
164
+ }
165
+ }
166
+
167
+ function stampAt(state, position) {
168
+ let latest = null;
169
+ while (state.stamps[0]?.position <= position) latest = state.stamps.shift();
170
+ return latest;
171
+ }
172
+
173
+ function addresses(state, from, to) {
174
+ const ranges = state.mappings.filter(mapping => mapping.from < to && mapping.to > from).map(mapping => [
175
+ mapping.sourceStart + Math.max(from, mapping.from) - mapping.from,
176
+ mapping.sourceStart + Math.min(to, mapping.to) - mapping.from - 1
177
+ ]);
178
+ if (ranges.reduce((sum, [a, b]) => sum + b - a + 1, 0) !== to - from) throw new Error("MPEG elementary bytes have missing addresses.");
179
+ return ranges;
180
+ }
181
+
182
+ function adtsFrame(bytes) {
183
+ if (bytes[0] !== 0xff || (bytes[1] & 0xf6) !== 0xf0) throw new Error("AAC ADTS header is invalid.");
184
+ const objectType = (bytes[2] >> 6) + 1;
185
+ const frequencyIndex = (bytes[2] >> 2) & 15;
186
+ const sampleRate = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350][frequencyIndex];
187
+ const channelConfiguration = ((bytes[2] & 1) << 2) | (bytes[3] >> 6);
188
+ if (!sampleRate || !channelConfiguration) throw new Error("AAC ADTS requires a declared sample rate and channel configuration.");
189
+ if (bytes[6] & 3) throw new Error("AAC ADTS multiple raw blocks require block-specific addresses.");
190
+ const headerBytes = bytes[1] & 1 ? 7 : 9;
191
+ const size = ((bytes[3] & 3) << 11) | (bytes[4] << 3) | (bytes[5] >> 5);
192
+ if (size <= headerBytes) throw new Error("AAC ADTS frame excludes its payload.");
193
+ const configuration = Buffer.from([(objectType << 3) | (frequencyIndex >> 1), ((frequencyIndex & 1) << 7) | (channelConfiguration << 3)]);
194
+ return { size, headerBytes, sampleRate, channels: channelConfiguration === 7 ? 8 : channelConfiguration,
195
+ duration: 1024 / sampleRate, codecId: "aac", codecPrivateB64: configuration.toString("base64") };
196
+ }
@@ -0,0 +1,69 @@
1
+ import { mpegVideoConfiguration } from "./mpeg-video-configuration.js";
2
+
3
+ /** Retain sequence declarations only; preceding user data is not decoder configuration. */
4
+ export class MpegSequenceReader {
5
+ #codec;
6
+ #zeros = 0;
7
+ #code = null;
8
+ #expectCode = false;
9
+ #unit = [];
10
+ #sequence = null;
11
+ #facts = null;
12
+
13
+ constructor(codec) { this.#codec = codec; }
14
+
15
+ push(bytes) {
16
+ for (const byte of bytes) {
17
+ if (this.#expectCode) {
18
+ this.#expectCode = false;
19
+ this.#code = byte;
20
+ this.#unit = [0, 0, 1, byte];
21
+ if (byte === 0xb3) this.#facts = null;
22
+ continue;
23
+ }
24
+ if (byte === 0) { this.#zeros++; continue; }
25
+ if (byte === 1 && this.#zeros >= 2) {
26
+ this.#appendZeros(this.#zeros - 2);
27
+ this.#finishUnit();
28
+ this.#zeros = 0;
29
+ this.#expectCode = true;
30
+ continue;
31
+ }
32
+ this.#appendZeros(this.#zeros);
33
+ this.#zeros = 0;
34
+ this.#append(byte);
35
+ this.#update();
36
+ }
37
+ return this.#facts;
38
+ }
39
+
40
+ #appendZeros(count) {
41
+ const remaining = this.#limit() - this.#unit.length;
42
+ for (let at = 0; at < Math.min(count, remaining); at++) this.#unit.push(0);
43
+ this.#update();
44
+ }
45
+
46
+ #limit() { return this.#code === 0xb3 ? 140 : this.#code === 0xb5 ? 10 : 0; }
47
+ #append(byte) { if (this.#unit.length < this.#limit()) this.#unit.push(byte); }
48
+
49
+ #finishUnit() {
50
+ if (this.#code === 0xb3) {
51
+ this.#sequence = Buffer.from(this.#unit);
52
+ this.#update();
53
+ }
54
+ if (this.#code === 0xb5) this.#update();
55
+ }
56
+
57
+ #update() {
58
+ const sequence = this.#code === 0xb3 ? Buffer.from(this.#unit) : this.#sequence;
59
+ if (!sequence) return;
60
+ const extension = this.#code === 0xb5 && this.#unit[4] >> 4 === 1 ? Buffer.from(this.#unit) : null;
61
+ if (this.#codec === "mpeg2video" && !extension) return;
62
+ try {
63
+ const facts = mpegVideoConfiguration(extension ? Buffer.concat([sequence, extension]) : sequence);
64
+ if (facts && (this.#codec !== "mpeg2video" || facts.codecId === "mpeg2video")) this.#facts = facts;
65
+ } catch (error) {
66
+ if (!error.message.includes("truncated")) throw error;
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,93 @@
1
+ import { pesPayload } from "./pes-header.js";
2
+ import { MpegElementaryIndex } from "./mpeg-elementary-index.js";
3
+
4
+ /** Incremental TS demultiplexing with exact elementary-payload addresses. */
5
+ export async function readMpegTsPackets({ readRange, fileSize, layout, tracks, state, interval, stopWhen, packetMemory }) {
6
+ state.elementary ??= new MpegElementaryIndex(tracks, { packetMemory });
7
+ state.pids ??= new Map();
8
+ state.offset ??= 0;
9
+ state.elementary.index.flushPending();
10
+ if (state.complete) return state.elementary.index;
11
+ if (interval && ready(state.elementary.index, tracks, interval)) return state.elementary.index;
12
+ const wanted = new Set(tracks.filter(track => ["audio", "video"].includes(track.type)).map(track => track.trackNumber));
13
+ const { width, sync } = layout;
14
+ while (state.offset + width <= fileSize) {
15
+ const sourceStart = state.offset + sync;
16
+ const packet = await readRange(sourceStart, sourceStart + 187);
17
+ if (packet[0] !== 0x47 || packet[1] & 0x80) throw new Error("MPEG-TS media packet is damaged.");
18
+ const pid = ((packet[1] & 31) << 8) | packet[2];
19
+ const control = (packet[3] >> 4) & 3;
20
+ if (!control) throw new Error("MPEG-TS adaptation control is invalid.");
21
+ const offset = control === 3 ? 5 + packet[4] : 4;
22
+ if (offset > 188 || (control === 2 && packet[4] > 183)) throw new Error("MPEG-TS adaptation field exceeds its packet.");
23
+ if (wanted.has(pid) && control !== 2 && offset < 188) {
24
+ if (packet[3] & 0xc0) throw new Error("MPEG-TS media is scrambled.");
25
+ let pes = state.pids.get(pid);
26
+ const counter = packet[3] & 15;
27
+ const duplicate = pes?.counter === counter;
28
+ const discontinuity = control === 3 && packet[4] > 0 && (packet[5] & 0x80) !== 0;
29
+ if (!duplicate) {
30
+ if (pes && !discontinuity && counter !== ((pes.counter + 1) & 15)) throw new Error("MPEG-TS media has missing packets.");
31
+ if (packet[1] & 0x40) {
32
+ if (pes?.remaining > 0) throw new Error("MPEG-TS PES ended before its declared length.");
33
+ pes = { counter, parts: [], header: null, remaining: null, timestampGiven: false };
34
+ state.pids.set(pid, pes);
35
+ }
36
+ if (pes) {
37
+ pes.counter = counter;
38
+ feed(state.elementary, pid, pes, packet.subarray(offset), sourceStart + offset);
39
+ }
40
+ }
41
+ }
42
+ state.offset += width;
43
+ state.elementary.index.flushPending();
44
+ if (stopWhen?.(state.elementary.index)) return state.elementary.index;
45
+ if (interval && ready(state.elementary.index, tracks, interval)) return state.elementary.index;
46
+ }
47
+ if (state.offset !== fileSize) throw new Error("MPEG-TS ends inside a transport packet.");
48
+ for (const pes of state.pids.values()) {
49
+ if (!pes.header || pes.remaining > 0) throw new Error("MPEG-TS ends inside a PES packet.");
50
+ }
51
+ const index = state.elementary.complete();
52
+ state.complete = true;
53
+ index.flushPending();
54
+ return index;
55
+ }
56
+
57
+ function feed(index, pid, pes, bytes, sourceStart) {
58
+ if (pes.header) return payload(index, pid, pes, bytes, sourceStart);
59
+ pes.parts.push({ bytes, sourceStart });
60
+ const header = Buffer.concat(pes.parts.map(part => part.bytes));
61
+ if (header.length < 9) return;
62
+ if (header[0] !== 0 || header[1] !== 0 || header[2] !== 1 || (header[6] & 0xc0) !== 0x80) throw new Error("MPEG-TS PES header is invalid.");
63
+ const size = 9 + header[8];
64
+ if (header.length < size) return;
65
+ const optional = pesPayload(header.subarray(6, size));
66
+ const declared = header.readUInt16BE(4);
67
+ if (!declared && (header[3] < 0xe0 || header[3] > 0xef)) throw new Error("Only MPEG-TS video PES may omit its length.");
68
+ if (declared && declared < size - 6) throw new Error("MPEG-TS PES length excludes its header.");
69
+ pes.header = optional;
70
+ pes.remaining = declared ? declared - (size - 6) : null;
71
+ let skip = size;
72
+ for (const part of pes.parts) {
73
+ const count = Math.min(skip, part.bytes.length);
74
+ skip -= count;
75
+ if (count < part.bytes.length) payload(index, pid, pes, part.bytes.subarray(count), part.sourceStart + count);
76
+ }
77
+ pes.parts = [];
78
+ }
79
+
80
+ function payload(index, pid, pes, bytes, sourceStart) {
81
+ const size = pes.remaining === null ? bytes.length : Math.min(pes.remaining, bytes.length);
82
+ if (!size) return;
83
+ index.push(pid, bytes.subarray(0, size), sourceStart, pes.timestampGiven ? {} : pes.header);
84
+ pes.timestampGiven = true;
85
+ if (pes.remaining !== null) pes.remaining -= size;
86
+ }
87
+
88
+ export function ready(index, tracks, interval) {
89
+ const results = tracks.filter(track => ["audio", "video"].includes(track.type) &&
90
+ (!interval.trackIds || interval.trackIds.includes(track.trackNumber))).map(track =>
91
+ index.inputFor({ trackId: track.trackNumber, from: interval.from, to: interval.to }));
92
+ return results.some(result => result.kind === "terminal") || results.every(result => result.kind === "result");
93
+ }
@@ -0,0 +1,50 @@
1
+ const RATES = [null, 24000 / 1001, 24, 25, 30000 / 1001, 30, 50, 60000 / 1001, 60];
2
+
3
+ /** Read sequence declarations from a bounded elementary-stream prefix. */
4
+ export function mpegVideoConfiguration(bytes) {
5
+ const sequence = bytes.indexOf(Buffer.from([0, 0, 1, 0xb3]));
6
+ if (sequence < 0) return null;
7
+ const bits = reader(bytes.subarray(sequence + 4));
8
+ let width = bits(12), height = bits(12);
9
+ const aspect = bits(4), rate = bits(4);
10
+ if (!width || !height || !aspect || !RATES[rate]) throw new Error("MPEG video sequence dimensions or rate are invalid.");
11
+ bits(18);
12
+ if (bits(1) !== 1) throw new Error("MPEG video sequence marker is invalid.");
13
+ bits(10); bits(1);
14
+ if (bits(1)) for (let index = 0; index < 64; index++) {
15
+ if (bits(8) === 0) throw new Error("MPEG video quantization matrix is invalid.");
16
+ }
17
+ if (bits(1)) for (let index = 0; index < 64; index++) {
18
+ if (bits(8) === 0) throw new Error("MPEG video quantization matrix is invalid.");
19
+ }
20
+ let fps = RATES[rate], codecId = "mpeg1video", progressiveSequence = true;
21
+ for (let at = sequence + 12; at + 4 < bytes.length; at++) {
22
+ if (bytes[at] !== 0 || bytes[at + 1] !== 0 || bytes[at + 2] !== 1) continue;
23
+ if (bytes[at + 3] === 0 || bytes[at + 3] === 0xb8) break;
24
+ if (bytes[at + 3] !== 0xb5 || bytes[at + 4] >> 4 !== 1) continue;
25
+ const extension = reader(bytes.subarray(at + 4));
26
+ extension(4); extension(8); progressiveSequence = extension(1) === 1;
27
+ if (extension(2) === 0) throw new Error("MPEG video chroma format is invalid.");
28
+ width += extension(2) * 4096;
29
+ height += extension(2) * 4096;
30
+ extension(12);
31
+ if (extension(1) !== 1) throw new Error("MPEG video extension marker is invalid.");
32
+ extension(8); extension(1);
33
+ fps *= (extension(2) + 1) / (extension(5) + 1);
34
+ codecId = "mpeg2video";
35
+ break;
36
+ }
37
+ return { width, height, fps, codecId, bitDepth: 8, progressiveSequence };
38
+ }
39
+
40
+ function reader(bytes) {
41
+ let offset = 0;
42
+ return count => {
43
+ if (offset + count > bytes.length * 8) throw new Error("MPEG video sequence header is truncated.");
44
+ let value = 0;
45
+ for (let index = 0; index < count; index++, offset++) {
46
+ value = value * 2 + ((bytes[offset >> 3] >> (7 - (offset & 7))) & 1);
47
+ }
48
+ return value;
49
+ };
50
+ }
@@ -0,0 +1,108 @@
1
+ import { Bits } from "./h264-configuration.js";
2
+ import { RetainedBytes } from "./RetainedBytes.js";
3
+
4
+ /** MPEG-4 Visual VOL/VOP clocks, committed only after packet admission. */
5
+ export class Mpeg4PictureTiming {
6
+ #state = { resolution: null, seconds: 0, previousSeconds: 0, origin: null, reference: null };
7
+ #memory;
8
+
9
+ constructor(memory) { this.#memory = memory; }
10
+
11
+ async read(readRange, start, end, startSeconds) {
12
+ let length = Math.min(4, end - start + 1);
13
+ while (true) {
14
+ const bytes = await readRange(start, start + length - 1);
15
+ try {
16
+ const picture = this.prepare(bytes, startSeconds);
17
+ if (!picture.packed) return { pictures: [{ ...picture, start, end }], packed: false, commit: picture.commit };
18
+ const allocation = new RetainedBytes(this.#memory);
19
+ try {
20
+ const full = await allocation.read(end - start + 1, () => readRange(start, end));
21
+ const starts = [];
22
+ for (let at = 0; at + 4 <= full.length; at++) if (!full[at] && !full[at + 1] && full[at + 2] === 1 && full[at + 3] === 0xb6) starts.push(at);
23
+ let state = this.#state;
24
+ const pictures = starts.map((at, position) => {
25
+ const first = position === 0 ? 0 : at, last = starts[position + 1] ?? full.length;
26
+ const one = this.prepare(full.subarray(first, last), startSeconds, state);
27
+ state = one.state;
28
+ return { ...one, start: start + first, end: start + last - 1 };
29
+ });
30
+ return { pictures, packed: true, commit: () => { this.#state = state; } };
31
+ } finally { allocation.dispose(); }
32
+ }
33
+ catch (error) {
34
+ if (length === end - start + 1 || !/bits are truncated|no complete VOP header/.test(error.message)) throw error;
35
+ length = Math.min(end - start + 1, length * 2);
36
+ }
37
+ }
38
+ }
39
+
40
+ prepare(bytes, startSeconds, previous = this.#state) {
41
+ const state = { ...previous };
42
+ state.packed ||= /DivX[^\0]*p(?=\0|$)/.test(bytes.toString("latin1"));
43
+ let picture = null;
44
+ for (let at = 0; at + 4 <= bytes.length; at++) {
45
+ if (bytes[at] || bytes[at + 1] || bytes[at + 2] !== 1) continue;
46
+ const code = bytes[at + 3];
47
+ if (code >= 0x20 && code <= 0x2f) state.resolution = resolutionOf(bytes.subarray(at + 4));
48
+ if (code === 0xb3) {
49
+ const bits = new Bits(bytes.subarray(at + 4), "MPEG-4 Visual");
50
+ const hours = bits.uint(5), minutes = bits.uint(6);
51
+ marker(bits);
52
+ const seconds = bits.uint(6);
53
+ if (hours > 23 || minutes > 59 || seconds > 59) throw new Error("MPEG-4 group clock is invalid.");
54
+ state.seconds = hours * 3600 + minutes * 60 + seconds;
55
+ }
56
+ if (code !== 0xb6) continue;
57
+ if (picture) {
58
+ if (state.packed) break;
59
+ throw new Error("Multiple MPEG-4 pictures require a packed-stream declaration.");
60
+ }
61
+ if (!(state.resolution > 0)) throw new Error("MPEG-4 picture precedes its VOL clock declaration.");
62
+ const bits = new Bits(bytes.subarray(at + 4), "MPEG-4 Visual");
63
+ const type = bits.uint(2);
64
+ let seconds = 0;
65
+ while (bits.uint(1)) seconds++;
66
+ marker(bits);
67
+ const increment = bits.uint(Math.max(1, Math.ceil(Math.log2(state.resolution))));
68
+ if (increment >= state.resolution) throw new Error("MPEG-4 picture clock exceeds its resolution.");
69
+ marker(bits);
70
+ const coded = bits.uint(1) === 1;
71
+ if (type !== 2) {
72
+ state.previousSeconds = state.seconds;
73
+ state.seconds += seconds;
74
+ }
75
+ const clock = (type === 2 ? state.previousSeconds + seconds : state.seconds) + increment / state.resolution;
76
+ state.origin ??= clock - startSeconds;
77
+ const pts = clock - state.origin;
78
+ const coveredThrough = type === 2 ? null : state.reference;
79
+ if (type !== 2) state.reference = pts;
80
+ picture = { pts, keyframe: type === 0, coded, coveredThrough };
81
+ }
82
+ if (!picture) throw new Error("MPEG-4 packet has no complete VOP header.");
83
+ return { ...picture, packed: state.packed === true, state, commit: () => { this.#state = state; } };
84
+ }
85
+ }
86
+
87
+ function resolutionOf(bytes) {
88
+ const bits = new Bits(bytes, "MPEG-4 Visual");
89
+ bits.uint(1); bits.uint(8);
90
+ if (bits.uint(1)) { bits.uint(4); bits.uint(3); }
91
+ if (bits.uint(4) === 15) { bits.uint(8); bits.uint(8); }
92
+ if (bits.uint(1)) {
93
+ bits.uint(2); bits.uint(1);
94
+ if (bits.uint(1)) {
95
+ bits.uint(15); marker(bits); bits.uint(15); marker(bits);
96
+ bits.uint(15); marker(bits); bits.uint(3); bits.uint(11); marker(bits);
97
+ bits.uint(15); marker(bits);
98
+ }
99
+ }
100
+ if (bits.uint(2) !== 0) throw new Error("MPEG-4 nonrectangular pictures require shape-specific timing.");
101
+ marker(bits);
102
+ const resolution = bits.uint(16);
103
+ marker(bits);
104
+ if (!resolution) throw new Error("MPEG-4 VOL clock resolution is zero.");
105
+ return resolution;
106
+ }
107
+
108
+ function marker(bits) { if (bits.uint(1) !== 1) throw new Error("MPEG-4 picture marker bit is invalid."); }
@@ -0,0 +1,34 @@
1
+ /** PES optional headers from ISO/IEC 13818-1; PTS is a 33-bit 90 kHz counter. */
2
+ export function pesPayload(bytes) {
3
+ let offset = 0, pts = null, dts = null;
4
+ if ((bytes[0] & 0xc0) === 0x80) {
5
+ if (bytes.length < 3 || 3 + bytes[2] > bytes.length) throw new Error("MPEG PES optional header is truncated.");
6
+ const flags = (bytes[1] >> 6) & 3;
7
+ if (flags === 1) throw new Error("MPEG PES timestamp flags are invalid.");
8
+ if (flags >= 2) {
9
+ if (bytes[2] < (flags === 3 ? 10 : 5)) throw new Error("MPEG PES timestamp is truncated.");
10
+ pts = pesTime(bytes.subarray(3, 8), flags);
11
+ dts = flags === 3 ? pesTime(bytes.subarray(8, 13), 1) : pts;
12
+ }
13
+ offset = 3 + bytes[2];
14
+ } else {
15
+ while (offset < bytes.length && bytes[offset] === 0xff) offset++;
16
+ if ((bytes[offset] & 0xc0) === 0x40) offset += 2;
17
+ const flags = bytes[offset] >> 4;
18
+ if (flags === 2 || flags === 3) {
19
+ const count = flags === 3 ? 10 : 5;
20
+ if (offset + count > bytes.length) throw new Error("MPEG-1 PES timestamp is truncated.");
21
+ pts = pesTime(bytes.subarray(offset, offset + 5), flags);
22
+ dts = flags === 3 ? pesTime(bytes.subarray(offset + 5, offset + 10), 1) : pts;
23
+ offset += count;
24
+ } else if (bytes[offset] === 0x0f) offset++;
25
+ else throw new Error("MPEG-1 PES optional header is invalid.");
26
+ }
27
+ return { offset, pts, dts };
28
+ }
29
+
30
+ function pesTime(bytes, prefix) {
31
+ if (bytes.length !== 5 || !(bytes[0] & bytes[2] & bytes[4] & 1)) throw new Error("MPEG PES timestamp markers are invalid.");
32
+ if ((bytes[0] >> 4) !== prefix) throw new Error("MPEG PES timestamp prefix is invalid.");
33
+ return ((bytes[0] & 14) * 2 ** 29 + bytes[1] * 2 ** 22 + (bytes[2] >> 1) * 2 ** 15 + bytes[3] * 2 ** 7 + (bytes[4] >> 1)) / 90000;
34
+ }
@@ -0,0 +1,57 @@
1
+ import { AudioTrack } from "./tracks/AudioTrack.js";
2
+
3
+ const VIDEO_CODECS = new Map([
4
+ ["V_MPEG4/ISO/AVC", "h264"], ["avc1", "h264"], ["avc3", "h264"],
5
+ ["V_MPEGH/ISO/HEVC", "hevc"], ["hvc1", "hevc"], ["hev1", "hevc"],
6
+ ["V_MPEG1", "mpeg1video"], ["V_MPEG2", "mpeg2video"],
7
+ ["V_MPEG4/ISO/ASP", "mpeg4"], ["V_MPEG4/ISO/SP", "mpeg4"], ["mp4v", "mpeg4"],
8
+ ["V_VP8", "vp8"], ["V_VP9", "vp9"], ["vp09", "vp9"],
9
+ ["V_AV1", "av1"], ["av01", "av1"], ["V_MJPEG", "mjpeg"],
10
+ ["V_FFV1", "ffv1"], ["V_THEORA", "theora"]
11
+ ]);
12
+ const SUBTITLE_CODECS = new Map([
13
+ ["S_TEXT/UTF8", "subrip"], ["S_TEXT/ASS", "ass"], ["S_TEXT/SSA", "ssa"],
14
+ ["S_TEXT/WEBVTT", "webvtt"], ["wvtt", "webvtt"], ["tx3g", "mov_text"],
15
+ ["S_HDMV/PGS", "hdmv_pgs_subtitle"], ["S_VOBSUB", "dvd_subtitle"]
16
+ ]);
17
+ const FOURCC_CODECS = new Map([
18
+ ["mjpg", "mjpeg"], ["jpeg", "mjpeg"], ["xvid", "mpeg4"],
19
+ ["divx", "mpeg4"], ["dx50", "mpeg4"], ["mpg2", "mpeg2video"]
20
+ ]);
21
+
22
+ function videoCodecOf(track) {
23
+ let id = track.codecId;
24
+ if (id === "V_MS/VFW/FOURCC" && track.codecPrivateB64) {
25
+ const header = Buffer.from(track.codecPrivateB64, "base64");
26
+ if (header.length >= 20) id = header.toString("ascii", 16, 20);
27
+ }
28
+ return VIDEO_CODECS.get(id) ?? FOURCC_CODECS.get(id.toLowerCase()) ?? id.toLowerCase();
29
+ }
30
+
31
+ /** One playback inventory, taken from the same declarations used for input. */
32
+ export function playbackDeclarations({ tracks, media, fileBytes = null }) {
33
+ const ordered = type => tracks.filter(track => track.type === type)
34
+ .sort((a, b) => a.declaredIndex - b.declaredIndex);
35
+ const video = ordered("video")[0];
36
+ const inventory = type => ordered(type).map((track, index) => ({
37
+ ...track, index, streamIndex: track.declaredIndex,
38
+ codec: type === "audio" ? AudioTrack.codecNameOf(track)
39
+ : SUBTITLE_CODECS.get(track.codecId) ?? track.codecId.toLowerCase(),
40
+ title: track.name ?? ""
41
+ }));
42
+ const audioTracks = inventory("audio"), subtitleTracks = inventory("subtitle");
43
+ return { tracks, video, audioTracks, subtitleTracks,
44
+ audioCodec: audioTracks[0]?.codec ?? "",
45
+ videoCodec: video ? videoCodecOf(video) : "",
46
+ container: media.format,
47
+ durationSeconds: media.durationSeconds,
48
+ startTimeSeconds: media.startTimeSeconds,
49
+ videoWidth: video?.width ?? 0, videoHeight: video?.height ?? 0,
50
+ fps: video?.fps ?? null, isHdr: video?.isHdr ?? false, bitDepth: video?.bitDepth ?? null,
51
+ // The measured whole-file average prices decoding; it never addresses packets.
52
+ bitrateKbps: Number.isFinite(media.bitrateKbps) ? media.bitrateKbps
53
+ : Number.isSafeInteger(fileBytes) && fileBytes > 0 && media.durationSeconds > 0
54
+ ? fileBytes * 8 / media.durationSeconds / 1000 : null,
55
+ streamCounts: { video: ordered("video").length, audio: audioTracks.length, subtitle: subtitleTracks.length }
56
+ };
57
+ }