@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,281 @@
|
|
|
1
|
+
import { pesPayload } from "./pes-header.js";
|
|
2
|
+
export { pesPayload } from "./pes-header.js";
|
|
3
|
+
import { Container } from "./Container.js";
|
|
4
|
+
import { VideoTrack } from "../tracks/VideoTrack.js";
|
|
5
|
+
import { AudioTrack } from "../tracks/AudioTrack.js";
|
|
6
|
+
import { ContainerTrack } from "../tracks/ContainerTrack.js";
|
|
7
|
+
import { mpegSectionCrc } from "./MpegTsContainer.js";
|
|
8
|
+
import { mpegVideoConfiguration } from "./mpeg-video-configuration.js";
|
|
9
|
+
import { MpegElementaryIndex } from "./mpeg-elementary-index.js";
|
|
10
|
+
import { ready } from "./mpeg-ts-packets.js";
|
|
11
|
+
|
|
12
|
+
const TYPES = new Map([[1, ["video", "mpeg1video"]], [2, ["video", "mpeg2video"]],
|
|
13
|
+
[0x10, ["video", "mpeg4"]], [0x1b, ["video", "h264"]], [0x24, ["video", "hevc"]],
|
|
14
|
+
[3, ["audio", "mp2"]], [4, ["audio", "mp2"]], [0x0f, ["audio", "aac"]],
|
|
15
|
+
[0x81, ["audio", "ac3"]], [0x87, ["audio", "eac3"]]]);
|
|
16
|
+
|
|
17
|
+
/** Program stream declarations and PES headers, without source-read waits. */
|
|
18
|
+
export class MpegPsContainer extends Container {
|
|
19
|
+
#offset = 0;
|
|
20
|
+
#streams = new Map();
|
|
21
|
+
#tracks = null;
|
|
22
|
+
#mapRead = false;
|
|
23
|
+
#firstTime = null;
|
|
24
|
+
#lastTime = null;
|
|
25
|
+
#videoPrefixes = new Map();
|
|
26
|
+
#packetState = { offset: 0, current: null, elementary: null, complete: false };
|
|
27
|
+
#duration = null;
|
|
28
|
+
|
|
29
|
+
get formatName() { return "mpegps"; }
|
|
30
|
+
packetIndexBytes() { return this.#packetState.elementary?.index.allocatedBytes() ?? 0; }
|
|
31
|
+
static detect(head) {
|
|
32
|
+
return head.length >= 5 && head[0] === 0 && head[1] === 0 && head[2] === 1 &&
|
|
33
|
+
(head[3] === 0xba || head[3] === 0xbb);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async readTracks() {
|
|
37
|
+
if (this.#tracks) return this.#tracks;
|
|
38
|
+
await this.#scan(true);
|
|
39
|
+
const counts = new Map();
|
|
40
|
+
this.#tracks = [...this.#streams].map(([trackNumber, stream]) => {
|
|
41
|
+
const declaredIndex = counts.get(stream.type) ?? 0;
|
|
42
|
+
counts.set(stream.type, declaredIndex + 1);
|
|
43
|
+
const params = { ...stream, trackNumber, declaredIndex, isEnabled: true, isDefault: false, declaresDefault: false };
|
|
44
|
+
return stream.type === "video" ? new VideoTrack(params) : stream.type === "audio" ? new AudioTrack(params) : new ContainerTrack(params);
|
|
45
|
+
});
|
|
46
|
+
return this.#tracks;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async readMediaInfo() {
|
|
50
|
+
const tracks = await this.readTracks();
|
|
51
|
+
const picture = tracks.find(track => track.type === "video");
|
|
52
|
+
if (["h264", "hevc"].includes(picture?.codecId)) {
|
|
53
|
+
const index = await this.#readPackets(tracks, null, () => picture.codecPrivateB64 &&
|
|
54
|
+
this.#packetState.elementary.index.boundsOf(picture.trackNumber) !== null);
|
|
55
|
+
const bounds = index.boundsOf(picture.trackNumber);
|
|
56
|
+
return { format: this.formatName, startTimeSeconds: bounds?.start ?? null,
|
|
57
|
+
durationSeconds: this.#duration };
|
|
58
|
+
}
|
|
59
|
+
if (picture && (this.#videoPrefixes.size === 0 || this.#firstTime === null)) await this.#scan(false, true);
|
|
60
|
+
for (const track of tracks.filter(track => track.type === "video")) {
|
|
61
|
+
const prefix = this.#videoPrefixes.get(track.trackNumber);
|
|
62
|
+
const facts = prefix && mpegVideoConfiguration(prefix);
|
|
63
|
+
if (facts) Object.assign(track, facts, { codecId: track.codecId || facts.codecId });
|
|
64
|
+
}
|
|
65
|
+
return { format: this.formatName, startTimeSeconds: this.#firstTime,
|
|
66
|
+
durationSeconds: this.#duration };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async readPacketIndex(interval) {
|
|
70
|
+
const tracks = await this.readTracks();
|
|
71
|
+
await this.readMediaInfo();
|
|
72
|
+
return this.#readPackets(tracks, interval);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async #readPackets(tracks, interval, stopWhen = null) {
|
|
76
|
+
const state = this.#packetState;
|
|
77
|
+
state.elementary ??= new MpegElementaryIndex(tracks, { packetMemory: this.packetMemory });
|
|
78
|
+
state.elementary.index.flushPending();
|
|
79
|
+
if (state.complete) {
|
|
80
|
+
this.#noteDuration(state.elementary.index, tracks);
|
|
81
|
+
return state.elementary.index;
|
|
82
|
+
}
|
|
83
|
+
if (interval && ready(state.elementary.index, tracks, interval)) return state.elementary.index;
|
|
84
|
+
while (state.offset < this.fileSize) {
|
|
85
|
+
if (!state.current) {
|
|
86
|
+
const prefix = await this.readRange(state.offset, state.offset + 3);
|
|
87
|
+
if (prefix.length !== 4 || prefix[0] !== 0 || prefix[1] !== 0 || prefix[2] !== 1) throw new Error("MPEG-PS lost packet synchronization.");
|
|
88
|
+
const id = prefix[3];
|
|
89
|
+
if (id === 0xb9) { state.offset += 4; break; }
|
|
90
|
+
if (id === 0xba) {
|
|
91
|
+
const version = await this.readRange(state.offset + 4, state.offset + 4);
|
|
92
|
+
let size;
|
|
93
|
+
if ((version[0] & 0xc0) === 0x40) {
|
|
94
|
+
const pack = await this.readRange(state.offset + 4, state.offset + 13);
|
|
95
|
+
size = 14 + (pack[9] & 7);
|
|
96
|
+
} else if ((version[0] & 0xf0) === 0x20) size = 12;
|
|
97
|
+
else throw new Error("MPEG-PS pack version is invalid.");
|
|
98
|
+
if (state.offset + size > this.fileSize) throw new Error("MPEG-PS pack exceeds the file.");
|
|
99
|
+
state.offset += size;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const sizeBytes = await this.readRange(state.offset + 4, state.offset + 5);
|
|
103
|
+
const size = sizeBytes.readUInt16BE();
|
|
104
|
+
if (!size && (id < 0xe0 || id > 0xef)) throw new Error("Only MPEG-PS video PES may omit its length.");
|
|
105
|
+
const end = size ? state.offset + 6 + size : await this.#nextPacket(state.offset + 6);
|
|
106
|
+
if (end > this.fileSize) throw new Error("MPEG-PS packet exceeds the file.");
|
|
107
|
+
if (id !== 0xbd && !tracks.some(track => track.trackNumber === id && ["video", "audio"].includes(track.type))) {
|
|
108
|
+
state.offset = end;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const headerBytes = await this.readRange(state.offset + 6, Math.min(end - 1, state.offset + 270));
|
|
112
|
+
const header = pesPayload(headerBytes);
|
|
113
|
+
let start = state.offset + 6 + header.offset;
|
|
114
|
+
let trackId = id;
|
|
115
|
+
if (id === 0xbd) {
|
|
116
|
+
if (start + 4 > end) throw new Error("MPEG-PS private audio header is truncated.");
|
|
117
|
+
const privateHeader = await this.readRange(start, start + 3);
|
|
118
|
+
trackId = privateHeader[0];
|
|
119
|
+
if (!tracks.some(track => track.trackNumber === trackId && track.type === "audio" && ["ac3", "dts"].includes(track.codecId))) {
|
|
120
|
+
state.offset = end;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
start += 4;
|
|
124
|
+
}
|
|
125
|
+
if (start > end) throw new Error("MPEG-PS PES header exceeds its packet.");
|
|
126
|
+
state.current = { id: trackId, end, cursor: start, header, timestampGiven: false };
|
|
127
|
+
}
|
|
128
|
+
const current = state.current;
|
|
129
|
+
while (current.cursor < current.end) {
|
|
130
|
+
const size = Math.min(current.end - current.cursor, this.portionBytes);
|
|
131
|
+
const bytes = await this.readRange(current.cursor, current.cursor + size - 1);
|
|
132
|
+
state.elementary.push(current.id, bytes, current.cursor, current.timestampGiven ? {} : current.header);
|
|
133
|
+
current.timestampGiven = true;
|
|
134
|
+
current.cursor += size;
|
|
135
|
+
state.elementary.index.flushPending();
|
|
136
|
+
if (stopWhen?.()) return state.elementary.index;
|
|
137
|
+
}
|
|
138
|
+
state.offset = current.end;
|
|
139
|
+
state.current = null;
|
|
140
|
+
if (interval && ready(state.elementary.index, tracks, interval)) return state.elementary.index;
|
|
141
|
+
}
|
|
142
|
+
if (state.offset !== this.fileSize) throw new Error("MPEG-PS has bytes after its program end.");
|
|
143
|
+
const index = state.elementary.complete();
|
|
144
|
+
state.complete = true;
|
|
145
|
+
index.flushPending();
|
|
146
|
+
this.#noteDuration(index, tracks);
|
|
147
|
+
return index;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#noteDuration(index, tracks) {
|
|
151
|
+
const timed = tracks.find(track => track.type === "video") ?? tracks.find(track => track.type === "audio");
|
|
152
|
+
const bounds = timed && index.boundsOf(timed.trackNumber);
|
|
153
|
+
if (bounds) {
|
|
154
|
+
this.#firstTime = bounds.start;
|
|
155
|
+
this.#duration = bounds.end - bounds.start;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async #scan(stopAtMap, stopAtPicture = false) {
|
|
160
|
+
while (this.#offset < this.fileSize) {
|
|
161
|
+
if (this.#offset + 4 > this.fileSize) throw new Error("MPEG-PS start code is truncated.");
|
|
162
|
+
const prefix = await this.readRange(this.#offset, this.#offset + 3);
|
|
163
|
+
if (prefix[0] !== 0 || prefix[1] !== 0 || prefix[2] !== 1) throw new Error("MPEG-PS lost packet synchronization.");
|
|
164
|
+
const id = prefix[3];
|
|
165
|
+
if (id === 0xb9) { this.#offset += 4; break; }
|
|
166
|
+
if (id === 0xba) {
|
|
167
|
+
const version = await this.readRange(this.#offset + 4, this.#offset + 4);
|
|
168
|
+
let size;
|
|
169
|
+
if ((version[0] & 0xc0) === 0x40) {
|
|
170
|
+
const pack = await this.readRange(this.#offset + 4, this.#offset + 13);
|
|
171
|
+
size = 14 + (pack[9] & 7);
|
|
172
|
+
} else if ((version[0] & 0xf0) === 0x20) size = 12;
|
|
173
|
+
else throw new Error("MPEG-PS pack version is invalid.");
|
|
174
|
+
if (this.#offset + size > this.fileSize) throw new Error("MPEG-PS pack exceeds the file.");
|
|
175
|
+
this.#offset += size;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const lengthBytes = await this.readRange(this.#offset + 4, this.#offset + 5);
|
|
179
|
+
const length = lengthBytes.readUInt16BE();
|
|
180
|
+
if (length === 0 && (id < 0xe0 || id > 0xef)) throw new Error("Only MPEG-PS video PES may omit its length.");
|
|
181
|
+
const end = length === 0 ? await this.#nextPacket(this.#offset + 6) : this.#offset + 6 + length;
|
|
182
|
+
if (end > this.fileSize) throw new Error("MPEG-PS packet exceeds the file.");
|
|
183
|
+
if (id === 0xbc) {
|
|
184
|
+
const map = await this.readRange(this.#offset, end - 1);
|
|
185
|
+
this.#programMap(map);
|
|
186
|
+
} else if ((id >= 0xc0 && id <= 0xef) || id === 0xbd || id === 0xfd) {
|
|
187
|
+
const bytes = await this.readRange(this.#offset + 6, Math.min(end - 1, this.#offset + 6 + 264));
|
|
188
|
+
const payload = pesPayload(bytes);
|
|
189
|
+
if (id >= 0xe0 && id <= 0xef && !this.#videoPrefixes.has(id) &&
|
|
190
|
+
bytes.indexOf(Buffer.from([0, 0, 1, 0xb3]), payload.offset) >= 0) {
|
|
191
|
+
this.#videoPrefixes.set(id, Buffer.from(bytes.subarray(payload.offset)));
|
|
192
|
+
}
|
|
193
|
+
if (payload.pts !== null) {
|
|
194
|
+
if (this.#firstTime === null) this.#firstTime = payload.pts;
|
|
195
|
+
this.#lastTime = Math.max(this.#lastTime ?? payload.pts, payload.pts);
|
|
196
|
+
}
|
|
197
|
+
if (!this.#mapRead) {
|
|
198
|
+
if (id >= 0xe0 && id <= 0xef) this.#streams.set(id, { type: "video", codecId: this.#streams.get(id)?.codecId || videoCodec(bytes.subarray(payload.offset)) });
|
|
199
|
+
else if (id >= 0xc0 && id <= 0xdf) this.#streams.set(id, { type: "audio", codecId: this.#streams.get(id)?.codecId || audioCodec(bytes.subarray(payload.offset)) });
|
|
200
|
+
else if (id === 0xfd) this.#streams.set(id, { type: "video", codecId: "" });
|
|
201
|
+
else if (payload.offset < bytes.length) {
|
|
202
|
+
const substream = bytes[payload.offset];
|
|
203
|
+
if (substream >= 0x80 && substream <= 0x87) this.#streams.set(substream, { type: "audio", codecId: "ac3" });
|
|
204
|
+
else if (substream >= 0x88 && substream <= 0x8f) this.#streams.set(substream, { type: "audio", codecId: "dts" });
|
|
205
|
+
else if (substream >= 0xa0 && substream <= 0xaf) this.#streams.set(substream, { type: "audio", codecId: "pcm_dvd" });
|
|
206
|
+
else if (substream >= 0x20 && substream <= 0x3f) this.#streams.set(substream, { type: "subtitle", codecId: "dvd_subtitle" });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.#offset = end;
|
|
211
|
+
if (stopAtMap && this.#mapRead) break;
|
|
212
|
+
if (stopAtPicture && this.#videoPrefixes.size > 0 && this.#firstTime !== null) break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async #nextPacket(start) {
|
|
217
|
+
let cursor = start, carry = Buffer.alloc(0);
|
|
218
|
+
while (cursor < this.fileSize) {
|
|
219
|
+
const width = Math.min(this.fileSize - cursor, this.portionBytes);
|
|
220
|
+
const chunk = await this.readRange(cursor, cursor + width - 1);
|
|
221
|
+
const bytes = Buffer.concat([carry, chunk]);
|
|
222
|
+
for (let at = 0; at + 3 < bytes.length; at++) {
|
|
223
|
+
// Elementary picture, sequence and GOP start codes remain PES payload.
|
|
224
|
+
if (bytes[at] === 0 && bytes[at + 1] === 0 && bytes[at + 2] === 1 && bytes[at + 3] >= 0xb9) {
|
|
225
|
+
return cursor - carry.length + at;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
carry = Buffer.from(bytes.subarray(Math.max(0, bytes.length - 3)));
|
|
229
|
+
cursor += width;
|
|
230
|
+
}
|
|
231
|
+
return this.fileSize;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#programMap(bytes) {
|
|
235
|
+
if (bytes.length < 16 || mpegSectionCrc(bytes) !== 0) throw new Error("MPEG-PS program map is invalid.");
|
|
236
|
+
if (!(bytes[6] & 0x80)) return;
|
|
237
|
+
const infoLength = bytes.readUInt16BE(8);
|
|
238
|
+
let at = 10 + infoLength;
|
|
239
|
+
if (at + 2 > bytes.length - 4) throw new Error("MPEG-PS program descriptors exceed its map.");
|
|
240
|
+
const end = at + 2 + bytes.readUInt16BE(at);
|
|
241
|
+
at += 2;
|
|
242
|
+
if (end !== bytes.length - 4) throw new Error("MPEG-PS elementary map length is invalid.");
|
|
243
|
+
const streams = new Map();
|
|
244
|
+
while (at < end) {
|
|
245
|
+
if (at + 4 > end) throw new Error("MPEG-PS stream declaration is truncated.");
|
|
246
|
+
const [type, codecId] = TYPES.get(bytes[at]) ?? ["other", ""];
|
|
247
|
+
const number = bytes[at + 1];
|
|
248
|
+
const next = at + 4 + bytes.readUInt16BE(at + 2);
|
|
249
|
+
if (next > end) throw new Error("MPEG-PS stream descriptors exceed its map.");
|
|
250
|
+
streams.set(number, { type, codecId });
|
|
251
|
+
at = next;
|
|
252
|
+
}
|
|
253
|
+
this.#streams = streams;
|
|
254
|
+
this.#mapRead = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function videoCodec(bytes) {
|
|
259
|
+
for (let at = 0; at + 4 < bytes.length; at++) {
|
|
260
|
+
if (bytes[at] === 0 && bytes[at + 1] === 0 && bytes[at + 2] === 1 &&
|
|
261
|
+
(bytes[at + 3] & 0x9f) === 7) return "h264";
|
|
262
|
+
}
|
|
263
|
+
const sequence = bytes.indexOf(Buffer.from([0, 0, 1, 0xb3]));
|
|
264
|
+
if (sequence < 0) return "";
|
|
265
|
+
const extension = bytes.indexOf(Buffer.from([0, 0, 1, 0xb5]), sequence + 4);
|
|
266
|
+
if (extension >= 0 && (bytes[extension + 4] >> 4) === 1) return "mpeg2video";
|
|
267
|
+
if (bytes.indexOf(Buffer.from([0, 0, 1, 0xb8]), sequence + 4) >= 0 ||
|
|
268
|
+
bytes.indexOf(Buffer.from([0, 0, 1, 0]), sequence + 4) >= 0) return "mpeg1video";
|
|
269
|
+
return "";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function audioCodec(bytes) {
|
|
273
|
+
for (let at = 0; at + 3 < bytes.length; at++) {
|
|
274
|
+
if (bytes[at] !== 0xff || (bytes[at + 1] & 0xe0) !== 0xe0) continue;
|
|
275
|
+
const layer = (bytes[at + 1] >> 1) & 3;
|
|
276
|
+
const version = (bytes[at + 1] >> 3) & 3;
|
|
277
|
+
if (version === 1 || layer === 0 || (bytes[at + 2] >> 4) === 15 || ((bytes[at + 2] >> 2) & 3) === 3) continue;
|
|
278
|
+
return layer === 1 ? "mp3" : layer === 2 ? "mp2" : "mp1";
|
|
279
|
+
}
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { Container } from "./Container.js";
|
|
2
|
+
import { VideoTrack } from "../tracks/VideoTrack.js";
|
|
3
|
+
import { AudioTrack } from "../tracks/AudioTrack.js";
|
|
4
|
+
import { ContainerTrack } from "../tracks/ContainerTrack.js";
|
|
5
|
+
import { pesPayload } from "./pes-header.js";
|
|
6
|
+
import { MpegSequenceReader } from "./mpeg-sequence-reader.js";
|
|
7
|
+
import { readMpegTsPackets } from "./mpeg-ts-packets.js";
|
|
8
|
+
|
|
9
|
+
// ISO/IEC 13818-1 stream_type values; private streams are identified by descriptors.
|
|
10
|
+
const STREAM_TYPES = new Map([
|
|
11
|
+
[0x01, ["video", "mpeg1video"]], [0x02, ["video", "mpeg2video"]],
|
|
12
|
+
[0x10, ["video", "mpeg4"]], [0x1b, ["video", "h264"]], [0x24, ["video", "hevc"]],
|
|
13
|
+
[0x03, ["audio", "mp2"]], [0x04, ["audio", "mp2"]],
|
|
14
|
+
[0x0f, ["audio", "aac"]], [0x11, ["audio", "aac_latm"]],
|
|
15
|
+
[0x81, ["audio", "ac3"]], [0x87, ["audio", "eac3"]], [0x8a, ["audio", "dts"]]
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/** Program and track declarations read packet by packet from existing bytes. */
|
|
19
|
+
export class MpegTsContainer extends Container {
|
|
20
|
+
#layout = null;
|
|
21
|
+
#offset = 0;
|
|
22
|
+
#sections = new Map();
|
|
23
|
+
#programs = new Map();
|
|
24
|
+
#programTables = new Map();
|
|
25
|
+
#patSections = new Set();
|
|
26
|
+
#lastPatSection = null;
|
|
27
|
+
#tracks = null;
|
|
28
|
+
#mediaOffset = 0;
|
|
29
|
+
#mediaPrefixes = new Map();
|
|
30
|
+
#mediaStart = null;
|
|
31
|
+
#mediaRead = false;
|
|
32
|
+
#packetState = {};
|
|
33
|
+
#duration = null;
|
|
34
|
+
|
|
35
|
+
get formatName() { return "mpegts"; }
|
|
36
|
+
packetIndexBytes() { return this.#packetState.elementary?.index.allocatedBytes() ?? 0; }
|
|
37
|
+
|
|
38
|
+
static detect(head) {
|
|
39
|
+
return (head[0] === 0x47 && (head[3] & 0x30) !== 0) ||
|
|
40
|
+
(head[4] === 0x47 && (head[7] & 0x30) !== 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async #packetLayout() {
|
|
44
|
+
if (this.#layout) return this.#layout;
|
|
45
|
+
const head = await this.readRange(0, Math.min(7, this.fileSize - 1));
|
|
46
|
+
const sync = head[0] === 0x47 ? 0 : head[4] === 0x47 ? 4 : -1;
|
|
47
|
+
if (sync < 0) throw new Error("MPEG-TS sync byte is absent.");
|
|
48
|
+
const widths = sync === 4 ? [192] : [188, 204];
|
|
49
|
+
for (const width of widths) {
|
|
50
|
+
const next = width + sync;
|
|
51
|
+
if (next >= this.fileSize) continue;
|
|
52
|
+
const byte = await this.readRange(next, next);
|
|
53
|
+
if (byte[0] !== 0x47) continue;
|
|
54
|
+
if (next + width < this.fileSize) {
|
|
55
|
+
const following = await this.readRange(next + width, next + width);
|
|
56
|
+
if (following[0] !== 0x47) continue;
|
|
57
|
+
}
|
|
58
|
+
this.#layout = { width, sync };
|
|
59
|
+
return this.#layout;
|
|
60
|
+
}
|
|
61
|
+
throw new Error("MPEG-TS packet spacing is invalid.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async readTracks() {
|
|
65
|
+
if (this.#tracks) return this.#tracks;
|
|
66
|
+
const { width, sync } = await this.#packetLayout();
|
|
67
|
+
while (this.#offset + width <= this.fileSize) {
|
|
68
|
+
const raw = await this.readRange(this.#offset + sync, this.#offset + sync + 187);
|
|
69
|
+
this.#consumePacket(raw);
|
|
70
|
+
this.#offset += width;
|
|
71
|
+
const patComplete = this.#lastPatSection !== null && this.#patSections.size === this.#lastPatSection + 1;
|
|
72
|
+
if (!patComplete || this.#programs.size === 0 ||
|
|
73
|
+
[...this.#programs.keys()].some(program => !this.#programTables.has(program))) continue;
|
|
74
|
+
const streams = [...this.#programs.keys()].flatMap(program => this.#programTables.get(program));
|
|
75
|
+
const counts = new Map();
|
|
76
|
+
this.#tracks = streams.map(stream => {
|
|
77
|
+
const declaredIndex = counts.get(stream.type) ?? 0;
|
|
78
|
+
counts.set(stream.type, declaredIndex + 1);
|
|
79
|
+
const params = { ...stream, trackNumber: stream.pid, declaredIndex, isEnabled: true, isDefault: false, declaresDefault: false };
|
|
80
|
+
if (stream.type === "video") return new VideoTrack(params);
|
|
81
|
+
if (stream.type === "audio") return new AudioTrack(params);
|
|
82
|
+
return new ContainerTrack(params);
|
|
83
|
+
});
|
|
84
|
+
return this.#tracks;
|
|
85
|
+
}
|
|
86
|
+
throw new Error("MPEG-TS ends before its program tables are complete.");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async readMediaInfo() {
|
|
90
|
+
const tracks = await this.readTracks();
|
|
91
|
+
if (this.#mediaRead) return { format: this.formatName, durationSeconds: this.#duration, startTimeSeconds: this.#mediaStart };
|
|
92
|
+
const video = tracks.find(track => track.type === "video");
|
|
93
|
+
if (["h264", "hevc"].includes(video?.codecId)) {
|
|
94
|
+
const index = await readMpegTsPackets({ readRange: this.readRange, fileSize: this.fileSize,
|
|
95
|
+
packetMemory: this.packetMemory,
|
|
96
|
+
layout: await this.#packetLayout(), tracks, state: this.#packetState,
|
|
97
|
+
stopWhen: value => video.codecPrivateB64 && value.boundsOf(video.trackNumber) !== null });
|
|
98
|
+
const bounds = index.boundsOf(video.trackNumber);
|
|
99
|
+
if (bounds) {
|
|
100
|
+
this.#mediaStart = bounds.start;
|
|
101
|
+
this.#mediaRead = true;
|
|
102
|
+
if (index.isComplete()) this.#duration = bounds.end - bounds.start;
|
|
103
|
+
}
|
|
104
|
+
return { format: this.formatName, durationSeconds: this.#duration, startTimeSeconds: this.#mediaStart };
|
|
105
|
+
}
|
|
106
|
+
if (!video) {
|
|
107
|
+
this.#mediaRead = true;
|
|
108
|
+
return { format: this.formatName, durationSeconds: null, startTimeSeconds: null };
|
|
109
|
+
}
|
|
110
|
+
const { width, sync } = await this.#packetLayout();
|
|
111
|
+
while (this.#mediaOffset + width <= this.fileSize) {
|
|
112
|
+
const packet = await this.readRange(this.#mediaOffset + sync, this.#mediaOffset + sync + 187);
|
|
113
|
+
const pid = ((packet[1] & 31) << 8) | packet[2];
|
|
114
|
+
if (packet[0] !== 0x47 || packet[1] & 0x80) throw new Error("MPEG-TS media packet is damaged.");
|
|
115
|
+
const control = (packet[3] >> 4) & 3;
|
|
116
|
+
if (!control) throw new Error("MPEG-TS adaptation control is invalid.");
|
|
117
|
+
const offset = control === 3 ? 5 + packet[4] : 4;
|
|
118
|
+
if (offset > 188) throw new Error("MPEG-TS adaptation field exceeds its packet.");
|
|
119
|
+
if (pid === video.trackNumber && control !== 2 && offset < 188) {
|
|
120
|
+
if (packet[3] & 0xc0) throw new Error("MPEG-TS media is scrambled.");
|
|
121
|
+
const payload = packet.subarray(offset);
|
|
122
|
+
let state = this.#mediaPrefixes.get(pid);
|
|
123
|
+
const counter = packet[3] & 15;
|
|
124
|
+
if (packet[1] & 0x40) state = { bytes: Buffer.alloc(0), header: null,
|
|
125
|
+
sequence: state?.sequence ?? new MpegSequenceReader(video.codecId), counter: (counter + 15) & 15 };
|
|
126
|
+
if (state && counter !== state.counter) {
|
|
127
|
+
if (counter !== ((state.counter + 1) & 15)) throw new Error("MPEG-TS media has missing packets.");
|
|
128
|
+
let elementary = payload;
|
|
129
|
+
if (!state.header) {
|
|
130
|
+
const prefix = Buffer.concat([state.bytes, payload]);
|
|
131
|
+
if (prefix.length >= 9) {
|
|
132
|
+
if (prefix[0] !== 0 || prefix[1] !== 0 || prefix[2] !== 1) throw new Error("MPEG-TS PES prefix is invalid.");
|
|
133
|
+
if ((prefix[6] & 0xc0) !== 0x80) throw new Error("MPEG-TS requires an MPEG-2 PES header.");
|
|
134
|
+
if (prefix.length >= 9 + prefix[8]) {
|
|
135
|
+
state.header = pesPayload(prefix.subarray(6));
|
|
136
|
+
elementary = prefix.subarray(6 + state.header.offset);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
state.bytes = state.header ? Buffer.alloc(0) : Buffer.from(prefix);
|
|
140
|
+
if (!state.header) elementary = Buffer.alloc(0);
|
|
141
|
+
}
|
|
142
|
+
const facts = ["mpeg1video", "mpeg2video"].includes(video.codecId) ? state.sequence.push(elementary) : null;
|
|
143
|
+
state.counter = counter;
|
|
144
|
+
this.#mediaPrefixes.set(pid, state);
|
|
145
|
+
if (state.header?.pts !== null && state.header?.pts !== undefined && this.#mediaStart === null) this.#mediaStart = state.header.pts;
|
|
146
|
+
if (facts) {
|
|
147
|
+
Object.assign(video, facts, { codecId: video.codecId || facts.codecId });
|
|
148
|
+
this.#mediaRead = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
this.#mediaOffset += width;
|
|
153
|
+
if (this.#mediaRead) break;
|
|
154
|
+
}
|
|
155
|
+
return { format: this.formatName, durationSeconds: null, startTimeSeconds: this.#mediaStart };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async readPacketIndex(interval) {
|
|
159
|
+
const tracks = await this.readTracks();
|
|
160
|
+
await this.readMediaInfo();
|
|
161
|
+
const index = await readMpegTsPackets({ readRange: this.readRange, fileSize: this.fileSize,
|
|
162
|
+
packetMemory: this.packetMemory,
|
|
163
|
+
layout: await this.#packetLayout(), tracks, state: this.#packetState, interval });
|
|
164
|
+
const timed = tracks.find(track => track.type === "video") ?? tracks.find(track => track.type === "audio");
|
|
165
|
+
if (timed && index.boundsOf(timed.trackNumber)) {
|
|
166
|
+
const bounds = index.boundsOf(timed.trackNumber);
|
|
167
|
+
this.#mediaStart = bounds.start;
|
|
168
|
+
if (index.isComplete()) this.#duration = bounds.end - bounds.start;
|
|
169
|
+
}
|
|
170
|
+
return index;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#consumePacket(packet) {
|
|
174
|
+
if (packet[0] !== 0x47) throw new Error("MPEG-TS packet lost synchronization.");
|
|
175
|
+
if (packet[1] & 0x80) throw new Error("MPEG-TS packet declares a transport error.");
|
|
176
|
+
const pid = ((packet[1] & 0x1f) << 8) | packet[2];
|
|
177
|
+
if (pid !== 0 && ![...this.#programs.values()].includes(pid)) return;
|
|
178
|
+
const control = (packet[3] >> 4) & 3;
|
|
179
|
+
if (control === 0) throw new Error("MPEG-TS adaptation control is invalid.");
|
|
180
|
+
if (control === 2) return;
|
|
181
|
+
if (packet[3] & 0xc0) throw new Error("MPEG-TS program table is scrambled.");
|
|
182
|
+
const offset = control === 3 ? 5 + packet[4] : 4;
|
|
183
|
+
if (offset > 188) throw new Error("MPEG-TS adaptation field exceeds its packet.");
|
|
184
|
+
if (offset === 188) return;
|
|
185
|
+
let state = this.#sections.get(pid);
|
|
186
|
+
if (!state) {
|
|
187
|
+
state = { bytes: Buffer.alloc(0), counter: null };
|
|
188
|
+
this.#sections.set(pid, state);
|
|
189
|
+
}
|
|
190
|
+
const counter = packet[3] & 15;
|
|
191
|
+
if (counter === state.counter) return;
|
|
192
|
+
const start = (packet[1] & 0x40) !== 0;
|
|
193
|
+
if (state.counter !== null && counter !== ((state.counter + 1) & 15) && state.bytes.length > 0) {
|
|
194
|
+
throw new Error("MPEG-TS program table has missing packets.");
|
|
195
|
+
}
|
|
196
|
+
state.counter = counter;
|
|
197
|
+
const payload = packet.subarray(offset);
|
|
198
|
+
if (start) {
|
|
199
|
+
const pointer = payload[0];
|
|
200
|
+
if (pointer + 1 > payload.length) throw new Error("MPEG-TS section pointer exceeds its packet.");
|
|
201
|
+
if (state.bytes.length) this.#appendSections(pid, state, payload.subarray(1, pointer + 1));
|
|
202
|
+
if (state.bytes.length) throw new Error("MPEG-TS section ended before its declared length.");
|
|
203
|
+
this.#appendSections(pid, state, payload.subarray(pointer + 1));
|
|
204
|
+
} else if (state.bytes.length) {
|
|
205
|
+
this.#appendSections(pid, state, payload);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#appendSections(pid, state, bytes) {
|
|
210
|
+
state.bytes = Buffer.concat([state.bytes, bytes]);
|
|
211
|
+
while (state.bytes.length > 0) {
|
|
212
|
+
if (state.bytes[0] === 0xff) { state.bytes = Buffer.alloc(0); return; }
|
|
213
|
+
if (state.bytes.length < 3) return;
|
|
214
|
+
const length = 3 + (((state.bytes[1] & 15) << 8) | state.bytes[2]);
|
|
215
|
+
if (length < 12 || length > 1024) throw new Error("MPEG-TS program section length is invalid.");
|
|
216
|
+
if (state.bytes.length < length) return;
|
|
217
|
+
const section = state.bytes.subarray(0, length);
|
|
218
|
+
state.bytes = state.bytes.subarray(length);
|
|
219
|
+
if (mpegSectionCrc(section) !== 0) throw new Error("MPEG-TS program section CRC is invalid.");
|
|
220
|
+
if (!(section[5] & 1)) continue;
|
|
221
|
+
if (pid === 0 && section[0] === 0) this.#readPat(section);
|
|
222
|
+
else if (section[0] === 2) this.#readPmt(section);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#readPat(section) {
|
|
227
|
+
this.#lastPatSection = section[7];
|
|
228
|
+
this.#patSections.add(section[6]);
|
|
229
|
+
for (let offset = 8; offset + 4 <= section.length - 4; offset += 4) {
|
|
230
|
+
const program = section.readUInt16BE(offset);
|
|
231
|
+
if (program !== 0) this.#programs.set(program, section.readUInt16BE(offset + 2) & 0x1fff);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#readPmt(section) {
|
|
236
|
+
const program = section.readUInt16BE(3);
|
|
237
|
+
if (!this.#programs.has(program)) return;
|
|
238
|
+
if (section[6] !== 0 || section[7] !== 0) throw new Error("MPEG-TS PMT section numbering is invalid.");
|
|
239
|
+
const streams = [];
|
|
240
|
+
let offset = 12 + (section.readUInt16BE(10) & 0x0fff);
|
|
241
|
+
while (offset < section.length - 4) {
|
|
242
|
+
if (offset + 5 > section.length - 4) throw new Error("MPEG-TS stream declaration is truncated.");
|
|
243
|
+
const streamType = section[offset];
|
|
244
|
+
const pid = section.readUInt16BE(offset + 1) & 0x1fff;
|
|
245
|
+
const end = offset + 5 + (section.readUInt16BE(offset + 3) & 0x0fff);
|
|
246
|
+
if (end > section.length - 4) throw new Error("MPEG-TS descriptor exceeds its section.");
|
|
247
|
+
let [type, codecId] = STREAM_TYPES.get(streamType) ?? ["other", ""];
|
|
248
|
+
let language = "";
|
|
249
|
+
for (let at = offset + 5; at < end;) {
|
|
250
|
+
if (at + 2 > end || at + 2 + section[at + 1] > end) throw new Error("MPEG-TS descriptor is truncated.");
|
|
251
|
+
const tag = section[at];
|
|
252
|
+
const data = section.subarray(at + 2, at + 2 + section[at + 1]);
|
|
253
|
+
if (tag === 0x0a && data.length >= 4) language = data.subarray(0, 3).toString("ascii");
|
|
254
|
+
if (streamType === 0x06) {
|
|
255
|
+
if (tag === 0x6a) [type, codecId] = ["audio", "ac3"];
|
|
256
|
+
if (tag === 0x7a) [type, codecId] = ["audio", "eac3"];
|
|
257
|
+
if (tag === 0x7b) [type, codecId] = ["audio", "dts"];
|
|
258
|
+
if (tag === 0x59) [type, codecId] = ["subtitle", "dvb_subtitle"];
|
|
259
|
+
}
|
|
260
|
+
at += 2 + data.length;
|
|
261
|
+
}
|
|
262
|
+
streams.push({ pid, type, codecId, language, declaresLanguage: language.length > 0 });
|
|
263
|
+
offset = end;
|
|
264
|
+
}
|
|
265
|
+
this.#programTables.set(program, streams);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** MPEG-2 section CRC-32: polynomial 0x04c11db7, no reflection or final XOR. */
|
|
270
|
+
export function mpegSectionCrc(bytes) {
|
|
271
|
+
let crc = 0xffffffff;
|
|
272
|
+
for (const byte of bytes) {
|
|
273
|
+
crc ^= byte << 24;
|
|
274
|
+
for (let bit = 0; bit < 8; bit++) crc = (crc << 1) ^ (crc & 0x80000000 ? 0x04c11db7 : 0);
|
|
275
|
+
}
|
|
276
|
+
return crc >>> 0;
|
|
277
|
+
}
|