@editframe/assets 0.47.0 → 0.47.2

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.
@@ -0,0 +1,323 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const require_Probe = require('./Probe.cjs');
3
+ let debug = require("debug");
4
+ debug = require_rolldown_runtime.__toESM(debug);
5
+ let node_fs_promises = require("node:fs/promises");
6
+ node_fs_promises = require_rolldown_runtime.__toESM(node_fs_promises);
7
+
8
+ //#region src/generateWebmSegmentIndex.ts
9
+ const log = (0, debug.default)("ef:generateWebmSegmentIndex");
10
+ const MIN_SEGMENT_DURATION_MS = 2e3;
11
+ const ID_EBML = 440786851;
12
+ const ID_SEGMENT = 408125543;
13
+ const ID_INFO = 357149030;
14
+ const ID_CLUSTER = 524531317;
15
+ const ID_TIMESTAMP = 231;
16
+ const ID_SIMPLE_BLOCK = 163;
17
+ const ID_BLOCK_GROUP = 160;
18
+ const ID_REFERENCE_BLOCK = 251;
19
+ const ID_TIMESTAMP_SCALE = 2807729;
20
+ /**
21
+ * Decode an EBML variable-length integer used for **element sizes**.
22
+ * The leading 1-bit encodes the byte-width; that bit is then stripped from
23
+ * the value. Returns `null` for the "unknown / streaming" sentinel value
24
+ * (all data-bits = 1).
25
+ */
26
+ function readVintSize(buf, offset) {
27
+ const b = buf[offset];
28
+ let width = 1;
29
+ let mask = 128;
30
+ while (width <= 8 && !(b & mask)) {
31
+ mask >>= 1;
32
+ width++;
33
+ }
34
+ let value = b & mask - 1;
35
+ for (let i = 1; i < width; i++) value = value << 8 | (buf[offset + i] ?? 0);
36
+ const unknownSentinel = (1 << 7 * width) - 1;
37
+ return {
38
+ value: value === unknownSentinel ? null : value,
39
+ width
40
+ };
41
+ }
42
+ /**
43
+ * Decode an EBML **element ID**. Unlike sizes, the leading marker bit is
44
+ * *kept* in the returned value (IDs are opaque bit patterns).
45
+ */
46
+ function readElementId(buf, offset) {
47
+ const b = buf[offset];
48
+ let width = 1;
49
+ let mask = 128;
50
+ while (width <= 4 && !(b & mask)) {
51
+ mask >>= 1;
52
+ width++;
53
+ }
54
+ let id = b;
55
+ for (let i = 1; i < width; i++) id = id << 8 | (buf[offset + i] ?? 0);
56
+ return {
57
+ id,
58
+ width
59
+ };
60
+ }
61
+ /** Read a big-endian unsigned integer of `size` bytes. */
62
+ function readUintBE(buf, offset, size) {
63
+ let val = 0;
64
+ for (let i = 0; i < size; i++) val = val * 256 + (buf[offset + i] ?? 0) >>> 0;
65
+ return val;
66
+ }
67
+ /**
68
+ * Parse the top-level EBML and Segment structure of a WebM file.
69
+ * Only reads as deep as needed: EBML header → Segment → Info/Tracks/Clusters.
70
+ */
71
+ function parseWebmStructure(buf) {
72
+ let pos = 0;
73
+ const { id: ebmlId, width: ebmlIdWidth } = readElementId(buf, pos);
74
+ if (ebmlId !== ID_EBML) throw new Error(`Expected EBML element (0x1A45DFA3), got 0x${ebmlId.toString(16)}`);
75
+ const { value: ebmlSize, width: ebmlSizeWidth } = readVintSize(buf, pos + ebmlIdWidth);
76
+ if (ebmlSize === null) throw new Error("EBML element has unknown size");
77
+ pos += ebmlIdWidth + ebmlSizeWidth + ebmlSize;
78
+ const { id: segId, width: segIdWidth } = readElementId(buf, pos);
79
+ if (segId !== ID_SEGMENT) throw new Error(`Expected Segment element (0x18538067), got 0x${segId.toString(16)}`);
80
+ const segmentSizeVintOffset = pos + segIdWidth;
81
+ const { width: segSizeWidth } = readVintSize(buf, segmentSizeVintOffset);
82
+ let spos = segmentSizeVintOffset + segSizeWidth;
83
+ let initSize = 0;
84
+ let timestampScaleNs = 1e6;
85
+ const clusters = [];
86
+ while (spos < buf.length) {
87
+ if (buf.length - spos < 2) break;
88
+ const { id: childId, width: childIdWidth } = readElementId(buf, spos);
89
+ const { value: childSize, width: childSizeWidth } = readVintSize(buf, spos + childIdWidth);
90
+ const headerSize = childIdWidth + childSizeWidth;
91
+ if (childSize === null) {
92
+ log(`Unknown-size element 0x${childId.toString(16)} at ${spos}, stopping`);
93
+ break;
94
+ }
95
+ const elementEnd = spos + headerSize + childSize;
96
+ if (childId === ID_INFO) {
97
+ let ipos = spos + headerSize;
98
+ while (ipos < elementEnd && ipos < buf.length) {
99
+ const { id: infoChildId, width: infoIdW } = readElementId(buf, ipos);
100
+ const { value: infoChildSize, width: infoSizeW } = readVintSize(buf, ipos + infoIdW);
101
+ if (infoChildSize === null) break;
102
+ if (infoChildId === ID_TIMESTAMP_SCALE) timestampScaleNs = readUintBE(buf, ipos + infoIdW + infoSizeW, infoChildSize);
103
+ ipos += infoIdW + infoSizeW + infoChildSize;
104
+ }
105
+ } else if (childId === ID_CLUSTER) {
106
+ if (initSize === 0) initSize = spos;
107
+ let clusterTimestampUnits = 0;
108
+ let hasKeyframe = false;
109
+ let cpos = spos + headerSize;
110
+ while (cpos < elementEnd && cpos < buf.length) {
111
+ if (buf.length - cpos < 2) break;
112
+ const { id: cid, width: cidW } = readElementId(buf, cpos);
113
+ const { value: csize, width: csizeW } = readVintSize(buf, cpos + cidW);
114
+ if (csize === null) break;
115
+ const cHeaderSize = cidW + csizeW;
116
+ if (cid === ID_TIMESTAMP) clusterTimestampUnits = readUintBE(buf, cpos + cHeaderSize, csize);
117
+ else if (cid === ID_SIMPLE_BLOCK) {
118
+ const sbOffset = cpos + cHeaderSize;
119
+ const { width: trackNumWidth } = readVintSize(buf, sbOffset);
120
+ if ((buf[sbOffset + trackNumWidth + 2] ?? 0) & 128) hasKeyframe = true;
121
+ } else if (cid === ID_BLOCK_GROUP) {
122
+ let bgpos = cpos + cHeaderSize;
123
+ const bgEnd = cpos + cHeaderSize + csize;
124
+ let hasReferenceBlock = false;
125
+ while (bgpos < bgEnd && bgpos < buf.length) {
126
+ const { id: bgid, width: bgidW } = readElementId(buf, bgpos);
127
+ const { value: bgsize, width: bgsizeW } = readVintSize(buf, bgpos + bgidW);
128
+ if (bgsize === null) break;
129
+ if (bgid === ID_REFERENCE_BLOCK) {
130
+ hasReferenceBlock = true;
131
+ break;
132
+ }
133
+ bgpos += bgidW + bgsizeW + bgsize;
134
+ }
135
+ if (!hasReferenceBlock) hasKeyframe = true;
136
+ }
137
+ cpos += cHeaderSize + csize;
138
+ }
139
+ const timestampMs = Math.round(clusterTimestampUnits * timestampScaleNs / 1e6);
140
+ clusters.push({
141
+ offset: spos,
142
+ size: headerSize + childSize,
143
+ timestampMs,
144
+ hasKeyframe
145
+ });
146
+ }
147
+ spos = elementEnd;
148
+ }
149
+ if (initSize === 0) throw new Error("No Cluster elements found in WebM file");
150
+ return {
151
+ segmentSizeVintOffset,
152
+ segmentSizeVintWidth: segSizeWidth,
153
+ initSize,
154
+ clusters,
155
+ timestampScaleNs
156
+ };
157
+ }
158
+ /**
159
+ * Use ffprobe packet data to mark clusters that contain keyframe packets.
160
+ * More reliable than parsing SimpleBlock flags in-process for large files.
161
+ */
162
+ async function annotateKeyframes(clusters, absolutePath) {
163
+ const keyframePositions = (await require_Probe.PacketProbe.probePath(absolutePath)).packets.filter((p) => p.flags?.includes("K") && p.pos !== void 0).map((p) => p.pos);
164
+ for (const cluster of clusters) {
165
+ const clusterEnd = cluster.offset + cluster.size;
166
+ cluster.hasKeyframe = keyframePositions.some((pos) => pos >= cluster.offset && pos < clusterEnd);
167
+ }
168
+ }
169
+ function buildSegments(clusters, timescale, totalDurationMs) {
170
+ const segments = [];
171
+ let segmentClusters = [];
172
+ let segmentStartMs = 0;
173
+ const flushSegment = (nextStartMs) => {
174
+ if (segmentClusters.length === 0) return;
175
+ const first = segmentClusters[0];
176
+ const last = segmentClusters[segmentClusters.length - 1];
177
+ const offset = first.offset;
178
+ const size = last.offset + last.size - first.offset;
179
+ const cts = Math.round(segmentStartMs * timescale / 1e3);
180
+ const dts = cts;
181
+ const duration = Math.round((nextStartMs - segmentStartMs) * timescale / 1e3);
182
+ segments.push({
183
+ cts,
184
+ dts,
185
+ offset,
186
+ size,
187
+ duration
188
+ });
189
+ segmentClusters = [];
190
+ };
191
+ for (let i = 0; i < clusters.length; i++) {
192
+ const cluster = clusters[i];
193
+ const durationSoFarMs = segmentClusters.length > 0 ? cluster.timestampMs - segmentStartMs : 0;
194
+ if (segmentClusters.length > 0 && durationSoFarMs >= MIN_SEGMENT_DURATION_MS && cluster.hasKeyframe) {
195
+ flushSegment(cluster.timestampMs);
196
+ segmentStartMs = cluster.timestampMs;
197
+ }
198
+ if (segmentClusters.length === 0) {
199
+ if (!cluster.hasKeyframe) continue;
200
+ segmentStartMs = cluster.timestampMs;
201
+ }
202
+ segmentClusters.push(cluster);
203
+ }
204
+ flushSegment(totalDurationMs);
205
+ return segments;
206
+ }
207
+ /**
208
+ * Build a TrackFragmentIndex for a VP9-alpha WebM source without any
209
+ * transcoding. The returned index contains byte offsets that point directly
210
+ * into the cached copy of the source file (which has its Segment-size VINT
211
+ * patched to "unknown" by generateTrack.ts).
212
+ */
213
+ async function generateWebmSegmentIndex(absolutePath, startTimeOffsetMs) {
214
+ log(`Generating WebM segment index for ${absolutePath}`);
215
+ const { initSize, clusters, timestampScaleNs } = parseWebmStructure(await (0, node_fs_promises.readFile)(absolutePath));
216
+ const timescale = 1e3;
217
+ log(`Parsed ${clusters.length} clusters, init=${initSize} bytes, timestampScale=${timestampScaleNs}ns`);
218
+ await annotateKeyframes(clusters, absolutePath);
219
+ log(`Keyframe clusters: ${clusters.filter((c) => c.hasKeyframe).map((c) => `${c.timestampMs}ms@${c.offset}`).join(", ")}`);
220
+ const lastCluster = clusters[clusters.length - 1];
221
+ let finalDurationMs = lastCluster.timestampMs + lastCluster.size / 4096;
222
+ try {
223
+ const videoStream$1 = (await require_Probe.PacketProbe.probePath(absolutePath)).data?.streams?.find((s) => s.codec_type === "video");
224
+ if (videoStream$1?.duration) finalDurationMs = Math.round(parseFloat(videoStream$1.duration) * 1e3);
225
+ } catch {}
226
+ log(`Total duration: ${finalDurationMs}ms`);
227
+ const segments = buildSegments(clusters, timescale, finalDurationMs);
228
+ log(`Built ${segments.length} segments: ${segments.map((s) => `[${s.cts / timescale * 1e3 | 0}ms offset=${s.offset} size=${s.size}]`).join(", ")}`);
229
+ const probe2 = await require_Probe.PacketProbe.probePath(absolutePath);
230
+ const videoStream = probe2.data?.streams?.find((s) => s.codec_type === "video");
231
+ const width = videoStream?.width ?? 1920;
232
+ const height = videoStream?.height ?? 1080;
233
+ const codec = videoStream?.codec_tag_string ?? videoStream?.codec_name ?? "vp09";
234
+ const sampleCount = probe2.packets?.filter((p) => p.codec_type !== "audio").length ?? 0;
235
+ let trackStartTimeOffsetMs;
236
+ if (startTimeOffsetMs !== void 0) trackStartTimeOffsetMs = startTimeOffsetMs;
237
+ else if (clusters[0] && clusters[0].timestampMs > 1) trackStartTimeOffsetMs = clusters[0].timestampMs;
238
+ const trackIndex = {
239
+ track: 1,
240
+ type: "video",
241
+ width,
242
+ height,
243
+ timescale,
244
+ codec,
245
+ duration: Math.round(finalDurationMs * timescale / 1e3),
246
+ sample_count: sampleCount,
247
+ startTimeOffsetMs: trackStartTimeOffsetMs,
248
+ initSegment: {
249
+ offset: 0,
250
+ size: initSize
251
+ },
252
+ segments
253
+ };
254
+ return { 1: trackIndex };
255
+ }
256
+ const ID_SEEKHEAD = 290298740;
257
+ const EBML_VOID_ID = 236;
258
+ /**
259
+ * Patch a WebM buffer in-place to make it safe for partial (segment-sliced) serving:
260
+ *
261
+ * 1. Segment-element size VINT → EBML unknown-size (0x01FFFFFFFFFFFFFF).
262
+ * Without this, mediabunny expects the full 44 MB when receiving a 5 MB chunk.
263
+ *
264
+ * 2. SeekHead element → EBML Void of the same byte length.
265
+ * The SeekHead contains Cues/cluster offsets relative to the original file.
266
+ * When mediabunny uses those offsets on a sliced buffer it reads garbage or
267
+ * falls off the end of the buffer, causing seek errors and de-sync. Replacing
268
+ * it with a Void element forces linear parsing: EBML → Info → Tracks → Clusters.
269
+ *
270
+ * Both patches are applied to a Buffer copy; the source file on disk is not modified.
271
+ */
272
+ function patchWebmForSegmentedServing(buf) {
273
+ try {
274
+ const { id: ebmlId, width: ebmlIdWidth } = readElementId(buf, 0);
275
+ if (ebmlId !== ID_EBML) return;
276
+ const { value: ebmlSize, width: ebmlSizeWidth } = readVintSize(buf, ebmlIdWidth);
277
+ if (ebmlSize === null) return;
278
+ const segPos = ebmlIdWidth + ebmlSizeWidth + ebmlSize;
279
+ const { id: segId, width: segIdWidth } = readElementId(buf, segPos);
280
+ if (segId !== ID_SEGMENT) return;
281
+ const segSizeOffset = segPos + segIdWidth;
282
+ const { width: segSizeWidth } = readVintSize(buf, segSizeOffset);
283
+ if (segSizeWidth === 8) Buffer.from([
284
+ 1,
285
+ 255,
286
+ 255,
287
+ 255,
288
+ 255,
289
+ 255,
290
+ 255,
291
+ 255
292
+ ]).copy(buf, segSizeOffset);
293
+ let pos = segPos + segIdWidth + segSizeWidth;
294
+ const scanLimit = Math.min(pos + 4096, buf.length);
295
+ while (pos < scanLimit) {
296
+ const { id: childId, width: childIdWidth } = readElementId(buf, pos);
297
+ const { value: childSize, width: childSizeWidth } = readVintSize(buf, pos + childIdWidth);
298
+ if (childSize === null) break;
299
+ const totalElementSize = childIdWidth + childSizeWidth + childSize;
300
+ if (childId === ID_SEEKHEAD) {
301
+ const voidContentSize = totalElementSize - 1;
302
+ let vintBytes;
303
+ if (voidContentSize <= 126) vintBytes = [128 | voidContentSize];
304
+ else if (voidContentSize <= 16382) vintBytes = [64 | voidContentSize >> 8, voidContentSize & 255];
305
+ else vintBytes = [
306
+ 32 | voidContentSize >> 16,
307
+ voidContentSize >> 8 & 255,
308
+ voidContentSize & 255
309
+ ];
310
+ buf[pos] = EBML_VOID_ID;
311
+ for (let i = 0; i < vintBytes.length; i++) buf[pos + 1 + i] = vintBytes[i];
312
+ buf.fill(0, pos + 1 + vintBytes.length, pos + totalElementSize);
313
+ break;
314
+ }
315
+ pos += totalElementSize;
316
+ }
317
+ } catch {}
318
+ }
319
+
320
+ //#endregion
321
+ exports.generateWebmSegmentIndex = generateWebmSegmentIndex;
322
+ exports.patchWebmForSegmentedServing = patchWebmForSegmentedServing;
323
+ //# sourceMappingURL=generateWebmSegmentIndex.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateWebmSegmentIndex.cjs","names":["clusters: ClusterInfo[]","PacketProbe","segments: TrackSegment[]","segmentClusters: ClusterInfo[]","videoStream","width: number","height: number","codec: string","sampleCount: number","trackStartTimeOffsetMs: number | undefined","trackIndex: TrackFragmentIndex","vintBytes: number[]"],"sources":["../src/generateWebmSegmentIndex.ts"],"sourcesContent":["/**\n * WebM / Matroska segment index generator.\n *\n * Instead of transcoding VP9-alpha WebM → fragmented MP4 (which strips the\n * alpha BlockAdditions), this module parses the source WebM's EBML structure\n * to produce byte-range segment maps directly into the original file.\n *\n * The pipeline:\n * 1. EBML parse: locate init region (EBML + Segment header + Info + Tracks)\n * and enumerate every Cluster with its timestamp.\n * 2. Keyframe detection: cross-reference cluster byte ranges with ffprobe\n * packet positions+flags to mark which clusters are keyframe-aligned.\n * 3. Segment grouping: accumulate clusters into ≥2-second segments that\n * start on keyframe clusters (mirrors generateFragmentIndex.ts logic).\n * 4. Output: same TrackFragmentIndex shape used by the MP4 path so the\n * rest of the pipeline (middleware, SegmentIndex.ts, EFVideo) is unchanged.\n *\n * Segment size patching\n * ---------------------\n * ffmpeg writes a known Segment-element size in the WebM header. When we\n * serve a subset of clusters, the size field must be replaced with the EBML\n * \"unknown\" value (0x01FFFFFFFFFFFFFF) so mediabunny does not expect more\n * bytes than we provide. This patch is applied once to the cached copy of\n * the source file (see generateTrack.ts).\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport debug from \"debug\";\nimport type { TrackFragmentIndex, TrackSegment } from \"./Probe.js\";\nimport { PacketProbe } from \"./Probe.js\";\n\nconst log = debug(\"ef:generateWebmSegmentIndex\");\n\nconst MIN_SEGMENT_DURATION_MS = 2000;\n\n// ---------------------------------------------------------------------------\n// EBML element IDs (big-endian, variable width)\n// ---------------------------------------------------------------------------\nconst ID_EBML = 0x1a45dfa3;\nconst ID_SEGMENT = 0x18538067;\nconst ID_INFO = 0x1549a966;\nconst ID_CLUSTER = 0x1f43b675;\nconst ID_TIMESTAMP = 0xe7; // inside Cluster\nconst ID_SIMPLE_BLOCK = 0xa3; // inside Cluster\nconst ID_BLOCK_GROUP = 0xa0; // inside Cluster (contains Block + optional ReferenceBlock)\nconst ID_REFERENCE_BLOCK = 0xfb; // inside BlockGroup — presence means delta frame\nconst ID_TIMESTAMP_SCALE = 0x2ad7b1; // inside Info (nanoseconds per timestamp unit)\n\n// ---------------------------------------------------------------------------\n// EBML primitive decoders\n// ---------------------------------------------------------------------------\n\n/**\n * Decode an EBML variable-length integer used for **element sizes**.\n * The leading 1-bit encodes the byte-width; that bit is then stripped from\n * the value. Returns `null` for the \"unknown / streaming\" sentinel value\n * (all data-bits = 1).\n */\nfunction readVintSize(buf: Buffer, offset: number): { value: number | null; width: number } {\n const b = buf[offset]!;\n let width = 1;\n let mask = 0x80;\n while (width <= 8 && !(b & mask)) {\n mask >>= 1;\n width++;\n }\n let value = b & (mask - 1); // strip leading marker bit\n for (let i = 1; i < width; i++) {\n value = (value << 8) | (buf[offset + i] ?? 0);\n }\n // All data-bits = 1 means \"unknown size\" in EBML\n const unknownSentinel = (1 << (7 * width)) - 1;\n return { value: value === unknownSentinel ? null : value, width };\n}\n\n/**\n * Decode an EBML **element ID**. Unlike sizes, the leading marker bit is\n * *kept* in the returned value (IDs are opaque bit patterns).\n */\nfunction readElementId(buf: Buffer, offset: number): { id: number; width: number } {\n const b = buf[offset]!;\n let width = 1;\n let mask = 0x80;\n while (width <= 4 && !(b & mask)) {\n mask >>= 1;\n width++;\n }\n let id = b;\n for (let i = 1; i < width; i++) {\n id = (id << 8) | (buf[offset + i] ?? 0);\n }\n return { id, width };\n}\n\n/** Read a big-endian unsigned integer of `size` bytes. */\nfunction readUintBE(buf: Buffer, offset: number, size: number): number {\n let val = 0;\n for (let i = 0; i < size; i++) {\n val = (val * 256 + (buf[offset + i] ?? 0)) >>> 0;\n }\n return val;\n}\n\n// ---------------------------------------------------------------------------\n// WebM structure parser\n// ---------------------------------------------------------------------------\n\ninterface ClusterInfo {\n /** Byte offset of the Cluster element's ID field in the file. */\n offset: number;\n /** Total byte length of the Cluster element (header + content). */\n size: number;\n /** Cluster base timestamp converted to milliseconds. */\n timestampMs: number;\n /** True if any SimpleBlock within the cluster has the keyframe flag set. */\n hasKeyframe: boolean;\n}\n\ninterface WebmStructure {\n /**\n * Byte offset of the 8-byte Segment-size VINT that must be patched to the\n * EBML unknown-size sentinel when serving partial content.\n */\n segmentSizeVintOffset: number;\n /** Byte length of the Segment-size VINT (should be 8 for ffmpeg output). */\n segmentSizeVintWidth: number;\n /** Byte offset of the first Cluster element == end of the \"init\" region. */\n initSize: number;\n /** All Cluster elements found in the file, in order. */\n clusters: ClusterInfo[];\n /** TimestampScale in nanoseconds per unit (default 1 000 000 = 1 ms/unit). */\n timestampScaleNs: number;\n}\n\n/**\n * Parse the top-level EBML and Segment structure of a WebM file.\n * Only reads as deep as needed: EBML header → Segment → Info/Tracks/Clusters.\n */\nfunction parseWebmStructure(buf: Buffer): WebmStructure {\n let pos = 0;\n\n // ── EBML element ─────────────────────────────────────────────────────────\n const { id: ebmlId, width: ebmlIdWidth } = readElementId(buf, pos);\n if (ebmlId !== ID_EBML) {\n throw new Error(`Expected EBML element (0x1A45DFA3), got 0x${ebmlId.toString(16)}`);\n }\n const { value: ebmlSize, width: ebmlSizeWidth } = readVintSize(buf, pos + ebmlIdWidth);\n if (ebmlSize === null) throw new Error(\"EBML element has unknown size\");\n pos += ebmlIdWidth + ebmlSizeWidth + ebmlSize; // skip to Segment\n\n // ── Segment element ───────────────────────────────────────────────────────\n const { id: segId, width: segIdWidth } = readElementId(buf, pos);\n if (segId !== ID_SEGMENT) {\n throw new Error(`Expected Segment element (0x18538067), got 0x${segId.toString(16)}`);\n }\n const segmentSizeVintOffset = pos + segIdWidth;\n const { width: segSizeWidth } = readVintSize(buf, segmentSizeVintOffset);\n const segContentStart = segmentSizeVintOffset + segSizeWidth;\n\n // ── Segment children ──────────────────────────────────────────────────────\n let spos = segContentStart;\n let initSize = 0;\n let timestampScaleNs = 1_000_000; // default: 1 ms per unit\n const clusters: ClusterInfo[] = [];\n\n while (spos < buf.length) {\n if (buf.length - spos < 2) break; // not enough bytes for a header\n\n const { id: childId, width: childIdWidth } = readElementId(buf, spos);\n const { value: childSize, width: childSizeWidth } = readVintSize(buf, spos + childIdWidth);\n const headerSize = childIdWidth + childSizeWidth;\n\n if (childSize === null) {\n // Unknown-size element (rare in well-formed WebM) — stop scanning.\n log(`Unknown-size element 0x${childId.toString(16)} at ${spos}, stopping`);\n break;\n }\n\n const elementEnd = spos + headerSize + childSize;\n\n if (childId === ID_INFO) {\n // Extract TimestampScale from Info\n let ipos = spos + headerSize;\n while (ipos < elementEnd && ipos < buf.length) {\n const { id: infoChildId, width: infoIdW } = readElementId(buf, ipos);\n const { value: infoChildSize, width: infoSizeW } = readVintSize(buf, ipos + infoIdW);\n if (infoChildSize === null) break;\n if (infoChildId === ID_TIMESTAMP_SCALE) {\n timestampScaleNs = readUintBE(buf, ipos + infoIdW + infoSizeW, infoChildSize);\n }\n ipos += infoIdW + infoSizeW + infoChildSize;\n }\n } else if (childId === ID_CLUSTER) {\n if (initSize === 0) {\n initSize = spos; // first cluster offset = init region ends here\n }\n\n // Extract Cluster Timestamp and scan for keyframe SimpleBlocks\n let clusterTimestampUnits = 0;\n let hasKeyframe = false;\n let cpos = spos + headerSize;\n\n while (cpos < elementEnd && cpos < buf.length) {\n if (buf.length - cpos < 2) break;\n const { id: cid, width: cidW } = readElementId(buf, cpos);\n const { value: csize, width: csizeW } = readVintSize(buf, cpos + cidW);\n if (csize === null) break;\n const cHeaderSize = cidW + csizeW;\n\n if (cid === ID_TIMESTAMP) {\n clusterTimestampUnits = readUintBE(buf, cpos + cHeaderSize, csize);\n } else if (cid === ID_SIMPLE_BLOCK) {\n // SimpleBlock layout: [track VINT][relative timecode int16BE][flags byte][frame data]\n const sbOffset = cpos + cHeaderSize;\n const { width: trackNumWidth } = readVintSize(buf, sbOffset);\n const flagsByte = buf[sbOffset + trackNumWidth + 2] ?? 0;\n if (flagsByte & 0x80) {\n hasKeyframe = true;\n }\n } else if (cid === ID_BLOCK_GROUP) {\n // BlockGroup: check for absence of ReferenceBlock → keyframe\n let bgpos = cpos + cHeaderSize;\n const bgEnd = cpos + cHeaderSize + csize;\n let hasReferenceBlock = false;\n while (bgpos < bgEnd && bgpos < buf.length) {\n const { id: bgid, width: bgidW } = readElementId(buf, bgpos);\n const { value: bgsize, width: bgsizeW } = readVintSize(buf, bgpos + bgidW);\n if (bgsize === null) break;\n if (bgid === ID_REFERENCE_BLOCK) {\n hasReferenceBlock = true;\n break;\n }\n bgpos += bgidW + bgsizeW + bgsize;\n }\n if (!hasReferenceBlock) hasKeyframe = true;\n }\n\n cpos += cHeaderSize + csize;\n }\n\n const timestampMs = Math.round((clusterTimestampUnits * timestampScaleNs) / 1_000_000);\n clusters.push({ offset: spos, size: headerSize + childSize, timestampMs, hasKeyframe });\n }\n\n spos = elementEnd;\n }\n\n if (initSize === 0) {\n throw new Error(\"No Cluster elements found in WebM file\");\n }\n\n return {\n segmentSizeVintOffset,\n segmentSizeVintWidth: segSizeWidth,\n initSize,\n clusters,\n timestampScaleNs,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Keyframe detection via ffprobe packet positions\n// ---------------------------------------------------------------------------\n\n/**\n * Use ffprobe packet data to mark clusters that contain keyframe packets.\n * More reliable than parsing SimpleBlock flags in-process for large files.\n */\nasync function annotateKeyframes(clusters: ClusterInfo[], absolutePath: string): Promise<void> {\n const probe = await PacketProbe.probePath(absolutePath);\n const keyframePositions = (probe.packets as Array<{ pos?: number; flags?: string }>)\n .filter((p) => p.flags?.includes(\"K\") && p.pos !== undefined)\n .map((p) => p.pos as number);\n\n for (const cluster of clusters) {\n const clusterEnd = cluster.offset + cluster.size;\n cluster.hasKeyframe = keyframePositions.some(\n (pos) => pos >= cluster.offset && pos < clusterEnd,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Segment grouping (mirrors generateFragmentIndex.ts logic)\n// ---------------------------------------------------------------------------\n\nfunction buildSegments(\n clusters: ClusterInfo[],\n timescale: number,\n totalDurationMs: number,\n): TrackSegment[] {\n const segments: TrackSegment[] = [];\n\n let segmentClusters: ClusterInfo[] = [];\n let segmentStartMs = 0;\n\n const flushSegment = (nextStartMs: number) => {\n if (segmentClusters.length === 0) return;\n const first = segmentClusters[0]!;\n const last = segmentClusters[segmentClusters.length - 1]!;\n const offset = first.offset;\n const size = last.offset + last.size - first.offset;\n const cts = Math.round((segmentStartMs * timescale) / 1000);\n const dts = cts;\n const duration = Math.round(((nextStartMs - segmentStartMs) * timescale) / 1000);\n segments.push({ cts, dts, offset, size, duration });\n segmentClusters = [];\n };\n\n for (let i = 0; i < clusters.length; i++) {\n const cluster = clusters[i]!;\n const durationSoFarMs = segmentClusters.length > 0 ? cluster.timestampMs - segmentStartMs : 0;\n\n const shouldFinalize =\n segmentClusters.length > 0 &&\n durationSoFarMs >= MIN_SEGMENT_DURATION_MS &&\n cluster.hasKeyframe;\n\n if (shouldFinalize) {\n flushSegment(cluster.timestampMs);\n segmentStartMs = cluster.timestampMs;\n }\n\n if (segmentClusters.length === 0) {\n if (!cluster.hasKeyframe) continue; // wait for first keyframe\n segmentStartMs = cluster.timestampMs;\n }\n\n segmentClusters.push(cluster);\n }\n\n // Flush final segment\n flushSegment(totalDurationMs);\n\n return segments;\n}\n\n// ---------------------------------------------------------------------------\n// Public entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Build a TrackFragmentIndex for a VP9-alpha WebM source without any\n * transcoding. The returned index contains byte offsets that point directly\n * into the cached copy of the source file (which has its Segment-size VINT\n * patched to \"unknown\" by generateTrack.ts).\n */\nexport async function generateWebmSegmentIndex(\n absolutePath: string,\n startTimeOffsetMs?: number,\n): Promise<Record<number, TrackFragmentIndex>> {\n log(`Generating WebM segment index for ${absolutePath}`);\n\n const buf = await readFile(absolutePath);\n const structure = parseWebmStructure(buf);\n\n const { initSize, clusters, timestampScaleNs } = structure;\n const timescale = 1000; // work in milliseconds\n\n log(\n `Parsed ${clusters.length} clusters, init=${initSize} bytes, ` +\n `timestampScale=${timestampScaleNs}ns`,\n );\n\n // Annotate keyframes via ffprobe for accuracy\n await annotateKeyframes(clusters, absolutePath);\n\n const keyframeClusters = clusters.filter((c) => c.hasKeyframe);\n log(\n `Keyframe clusters: ${keyframeClusters.map((c) => `${c.timestampMs}ms@${c.offset}`).join(\", \")}`,\n );\n\n // Total duration from last cluster\n const lastCluster = clusters[clusters.length - 1]!;\n const totalDurationMs = lastCluster.timestampMs + lastCluster.size / 4096; // rough estimate\n // Better: use ffprobe stream duration\n let finalDurationMs = totalDurationMs;\n try {\n const probe = await PacketProbe.probePath(absolutePath);\n const videoStream = (probe as any).data?.streams?.find((s: any) => s.codec_type === \"video\");\n if (videoStream?.duration) {\n finalDurationMs = Math.round(parseFloat(videoStream.duration) * 1000);\n }\n } catch {\n // fall back to rough estimate\n }\n\n log(`Total duration: ${finalDurationMs}ms`);\n\n const segments = buildSegments(clusters, timescale, finalDurationMs);\n\n log(\n `Built ${segments.length} segments: ${segments.map((s) => `[${((s.cts / timescale) * 1000) | 0}ms offset=${s.offset} size=${s.size}]`).join(\", \")}`,\n );\n\n // Track metadata from ffprobe\n const probe2 = await PacketProbe.probePath(absolutePath);\n const videoStream = (probe2 as any).data?.streams?.find((s: any) => s.codec_type === \"video\");\n const width: number = videoStream?.width ?? 1920;\n const height: number = videoStream?.height ?? 1080;\n const codec: string = videoStream?.codec_tag_string ?? videoStream?.codec_name ?? \"vp09\";\n const sampleCount: number =\n (probe2 as any).packets?.filter((p: any) => p.codec_type !== \"audio\").length ?? 0;\n\n let trackStartTimeOffsetMs: number | undefined;\n if (startTimeOffsetMs !== undefined) {\n trackStartTimeOffsetMs = startTimeOffsetMs;\n } else if (clusters[0] && clusters[0].timestampMs > 1) {\n trackStartTimeOffsetMs = clusters[0].timestampMs;\n }\n\n const trackIndex: TrackFragmentIndex = {\n track: 1,\n type: \"video\",\n width,\n height,\n timescale,\n codec,\n duration: Math.round((finalDurationMs * timescale) / 1000),\n sample_count: sampleCount,\n startTimeOffsetMs: trackStartTimeOffsetMs,\n initSegment: { offset: 0, size: initSize },\n segments,\n };\n\n return { 1: trackIndex };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers exported for use in generateTrack.ts\n// ---------------------------------------------------------------------------\n\nconst ID_SEEKHEAD = 0x114d9b74;\nconst EBML_VOID_ID = 0xec;\n\n/**\n * Patch a WebM buffer in-place to make it safe for partial (segment-sliced) serving:\n *\n * 1. Segment-element size VINT → EBML unknown-size (0x01FFFFFFFFFFFFFF).\n * Without this, mediabunny expects the full 44 MB when receiving a 5 MB chunk.\n *\n * 2. SeekHead element → EBML Void of the same byte length.\n * The SeekHead contains Cues/cluster offsets relative to the original file.\n * When mediabunny uses those offsets on a sliced buffer it reads garbage or\n * falls off the end of the buffer, causing seek errors and de-sync. Replacing\n * it with a Void element forces linear parsing: EBML → Info → Tracks → Clusters.\n *\n * Both patches are applied to a Buffer copy; the source file on disk is not modified.\n */\nexport function patchWebmForSegmentedServing(buf: Buffer): void {\n try {\n // ── Locate EBML and Segment headers ───────────────────────────────────\n const { id: ebmlId, width: ebmlIdWidth } = readElementId(buf, 0);\n if (ebmlId !== ID_EBML) return;\n const { value: ebmlSize, width: ebmlSizeWidth } = readVintSize(buf, ebmlIdWidth);\n if (ebmlSize === null) return;\n const segPos = ebmlIdWidth + ebmlSizeWidth + ebmlSize;\n const { id: segId, width: segIdWidth } = readElementId(buf, segPos);\n if (segId !== ID_SEGMENT) return;\n\n // ── Patch 1: Segment size → unknown ───────────────────────────────────\n const segSizeOffset = segPos + segIdWidth;\n const { width: segSizeWidth } = readVintSize(buf, segSizeOffset);\n if (segSizeWidth === 8) {\n const UNKNOWN_SIZE = Buffer.from([0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);\n UNKNOWN_SIZE.copy(buf, segSizeOffset);\n }\n\n // ── Patch 2: SeekHead → Void ──────────────────────────────────────────\n const segContentStart = segPos + segIdWidth + segSizeWidth;\n let pos = segContentStart;\n\n // Scan segment children until we find SeekHead (it is always the first child\n // in ffmpeg-generated WebM files, but scan defensively up to 4 KB).\n const scanLimit = Math.min(pos + 4096, buf.length);\n while (pos < scanLimit) {\n const { id: childId, width: childIdWidth } = readElementId(buf, pos);\n const { value: childSize, width: childSizeWidth } = readVintSize(buf, pos + childIdWidth);\n if (childSize === null) break;\n const totalElementSize = childIdWidth + childSizeWidth + childSize;\n\n if (childId === ID_SEEKHEAD) {\n // Overwrite the entire SeekHead element with a Void element of equal size.\n // Void layout: 1-byte ID (0xEC) + VINT size + zero content.\n // We need VINT(totalElementSize - 1) to account for the 1-byte Void ID.\n const voidContentSize = totalElementSize - 1; // after the 0xEC ID byte\n\n // Encode the size as a VINT. voidContentSize is small enough for 2-byte VINT\n // (max 16383) or even 1-byte (max 127) for typical SeekHead sizes.\n let vintBytes: number[];\n if (voidContentSize <= 126) {\n // 1-byte VINT: 0x80 | value\n vintBytes = [0x80 | voidContentSize];\n } else if (voidContentSize <= 16382) {\n // 2-byte VINT: 0x40xx\n vintBytes = [0x40 | (voidContentSize >> 8), voidContentSize & 0xff];\n } else {\n // 3-byte VINT (should never be needed for SeekHead)\n vintBytes = [\n 0x20 | (voidContentSize >> 16),\n (voidContentSize >> 8) & 0xff,\n voidContentSize & 0xff,\n ];\n }\n\n buf[pos] = EBML_VOID_ID;\n for (let i = 0; i < vintBytes.length; i++) {\n buf[pos + 1 + i] = vintBytes[i]!;\n }\n // Zero out the rest of the element\n buf.fill(0, pos + 1 + vintBytes.length, pos + totalElementSize);\n break; // only one SeekHead per file\n }\n\n pos += totalElementSize;\n }\n } catch {\n // Best-effort — if parsing fails the file is served unpatched\n }\n}\n"],"mappings":"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"}