@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,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
|
|
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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
|
42
|
-
import { ElementReader
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
246
|
-
|
|
247
|
-
|
|
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 =
|
|
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
|
|
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) {
|