@editframe/assets 0.40.0 → 0.40.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.
Files changed (33) hide show
  1. package/dist/Probe.cjs +28 -9
  2. package/dist/Probe.cjs.map +1 -1
  3. package/dist/Probe.d.cts +27 -26
  4. package/dist/Probe.d.ts +27 -26
  5. package/dist/Probe.js +29 -9
  6. package/dist/Probe.js.map +1 -1
  7. package/dist/VideoRenderOptions.d.cts +48 -48
  8. package/dist/VideoRenderOptions.d.ts +48 -48
  9. package/dist/generateFragmentIndex.cjs +39 -26
  10. package/dist/generateFragmentIndex.cjs.map +1 -1
  11. package/dist/generateFragmentIndex.d.cts +5 -1
  12. package/dist/generateFragmentIndex.d.ts +5 -1
  13. package/dist/generateFragmentIndex.js +35 -27
  14. package/dist/generateFragmentIndex.js.map +1 -1
  15. package/dist/generateSingleTrack.cjs.map +1 -1
  16. package/dist/generateSingleTrack.js.map +1 -1
  17. package/dist/idempotentTask.cjs +58 -6
  18. package/dist/idempotentTask.cjs.map +1 -1
  19. package/dist/idempotentTask.js +58 -7
  20. package/dist/idempotentTask.js.map +1 -1
  21. package/dist/tasks/findOrCreateCaptions.cjs +13 -6
  22. package/dist/tasks/findOrCreateCaptions.cjs.map +1 -1
  23. package/dist/tasks/findOrCreateCaptions.js +13 -6
  24. package/dist/tasks/findOrCreateCaptions.js.map +1 -1
  25. package/dist/tasks/generateScrubTrack.cjs +1 -11
  26. package/dist/tasks/generateScrubTrack.cjs.map +1 -1
  27. package/dist/tasks/generateScrubTrack.js +1 -11
  28. package/dist/tasks/generateScrubTrack.js.map +1 -1
  29. package/dist/tasks/generateTrackFragmentIndex.cjs +22 -28
  30. package/dist/tasks/generateTrackFragmentIndex.cjs.map +1 -1
  31. package/dist/tasks/generateTrackFragmentIndex.js +22 -28
  32. package/dist/tasks/generateTrackFragmentIndex.js.map +1 -1
  33. package/package.json +1 -1
@@ -76,6 +76,19 @@ declare const VideoRenderOptions: z.ZodObject<{
76
76
  numberOfChannels: number;
77
77
  }>;
78
78
  }, "strip", z.ZodTypeAny, {
79
+ audio: {
80
+ codec: string;
81
+ bitrate: number;
82
+ sampleRate: number;
83
+ numberOfChannels: number;
84
+ };
85
+ video: {
86
+ width: number;
87
+ height: number;
88
+ framerate: number;
89
+ codec: string;
90
+ bitrate: number;
91
+ };
79
92
  sequenceNumber: number;
80
93
  keyframeIntervalMs: number;
81
94
  fromMs: number;
@@ -85,6 +98,15 @@ declare const VideoRenderOptions: z.ZodObject<{
85
98
  alignedFromUs: number;
86
99
  alignedToUs: number;
87
100
  isInitSegment: boolean;
101
+ noVideo?: boolean | undefined;
102
+ noAudio?: boolean | undefined;
103
+ }, {
104
+ audio: {
105
+ codec: string;
106
+ bitrate: number;
107
+ sampleRate: number;
108
+ numberOfChannels: number;
109
+ };
88
110
  video: {
89
111
  width: number;
90
112
  height: number;
@@ -92,15 +114,6 @@ declare const VideoRenderOptions: z.ZodObject<{
92
114
  codec: string;
93
115
  bitrate: number;
94
116
  };
95
- audio: {
96
- codec: string;
97
- bitrate: number;
98
- sampleRate: number;
99
- numberOfChannels: number;
100
- };
101
- noVideo?: boolean | undefined;
102
- noAudio?: boolean | undefined;
103
- }, {
104
117
  sequenceNumber: number;
105
118
  keyframeIntervalMs: number;
106
119
  fromMs: number;
@@ -110,19 +123,6 @@ declare const VideoRenderOptions: z.ZodObject<{
110
123
  alignedFromUs: number;
111
124
  alignedToUs: number;
112
125
  isInitSegment: boolean;
113
- video: {
114
- width: number;
115
- height: number;
116
- framerate: number;
117
- codec: string;
118
- bitrate: number;
119
- };
120
- audio: {
121
- codec: string;
122
- bitrate: number;
123
- sampleRate: number;
124
- numberOfChannels: number;
125
- };
126
126
  noVideo?: boolean | undefined;
127
127
  noAudio?: boolean | undefined;
128
128
  }>;
@@ -131,6 +131,19 @@ declare const VideoRenderOptions: z.ZodObject<{
131
131
  mode: "canvas" | "screenshot";
132
132
  strategy: "v1" | "v2";
133
133
  encoderOptions: {
134
+ audio: {
135
+ codec: string;
136
+ bitrate: number;
137
+ sampleRate: number;
138
+ numberOfChannels: number;
139
+ };
140
+ video: {
141
+ width: number;
142
+ height: number;
143
+ framerate: number;
144
+ codec: string;
145
+ bitrate: number;
146
+ };
134
147
  sequenceNumber: number;
135
148
  keyframeIntervalMs: number;
136
149
  fromMs: number;
@@ -140,19 +153,6 @@ declare const VideoRenderOptions: z.ZodObject<{
140
153
  alignedFromUs: number;
141
154
  alignedToUs: number;
142
155
  isInitSegment: boolean;
143
- video: {
144
- width: number;
145
- height: number;
146
- framerate: number;
147
- codec: string;
148
- bitrate: number;
149
- };
150
- audio: {
151
- codec: string;
152
- bitrate: number;
153
- sampleRate: number;
154
- numberOfChannels: number;
155
- };
156
156
  noVideo?: boolean | undefined;
157
157
  noAudio?: boolean | undefined;
158
158
  };
@@ -163,6 +163,19 @@ declare const VideoRenderOptions: z.ZodObject<{
163
163
  mode: "canvas" | "screenshot";
164
164
  strategy: "v1" | "v2";
165
165
  encoderOptions: {
166
+ audio: {
167
+ codec: string;
168
+ bitrate: number;
169
+ sampleRate: number;
170
+ numberOfChannels: number;
171
+ };
172
+ video: {
173
+ width: number;
174
+ height: number;
175
+ framerate: number;
176
+ codec: string;
177
+ bitrate: number;
178
+ };
166
179
  sequenceNumber: number;
167
180
  keyframeIntervalMs: number;
168
181
  fromMs: number;
@@ -172,19 +185,6 @@ declare const VideoRenderOptions: z.ZodObject<{
172
185
  alignedFromUs: number;
173
186
  alignedToUs: number;
174
187
  isInitSegment: boolean;
175
- video: {
176
- width: number;
177
- height: number;
178
- framerate: number;
179
- codec: string;
180
- bitrate: number;
181
- };
182
- audio: {
183
- codec: string;
184
- bitrate: number;
185
- sampleRate: number;
186
- numberOfChannels: number;
187
- };
188
188
  noVideo?: boolean | undefined;
189
189
  noAudio?: boolean | undefined;
190
190
  };
@@ -1,11 +1,21 @@
1
1
  const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
2
  const require_Probe = require('./Probe.cjs');
3
+ let node_fs = require("node:fs");
4
+ node_fs = require_rolldown_runtime.__toESM(node_fs);
3
5
  let debug = require("debug");
4
6
  debug = require_rolldown_runtime.__toESM(debug);
5
7
  let node_stream = require("node:stream");
6
8
  node_stream = require_rolldown_runtime.__toESM(node_stream);
7
9
  let node_stream_promises = require("node:stream/promises");
8
10
  node_stream_promises = require_rolldown_runtime.__toESM(node_stream_promises);
11
+ let node_fs_promises = require("node:fs/promises");
12
+ node_fs_promises = require_rolldown_runtime.__toESM(node_fs_promises);
13
+ let node_os = require("node:os");
14
+ node_os = require_rolldown_runtime.__toESM(node_os);
15
+ let node_path = require("node:path");
16
+ node_path = require_rolldown_runtime.__toESM(node_path);
17
+ let node_crypto = require("node:crypto");
18
+ node_crypto = require_rolldown_runtime.__toESM(node_crypto);
9
19
 
10
20
  //#region src/generateFragmentIndex.ts
11
21
  const log = (0, debug.default)("ef:generateFragmentIndex");
@@ -99,19 +109,6 @@ var StreamingBoxParser = class extends node_stream.Transform {
99
109
  return this.fragments;
100
110
  }
101
111
  };
102
- function createFragmentStream(fragmentData) {
103
- let offset = 0;
104
- return new node_stream.Readable({ read() {
105
- if (offset >= fragmentData.length) {
106
- this.push(null);
107
- return;
108
- }
109
- const chunkSize = Math.min(64 * 1024, fragmentData.length - offset);
110
- const chunk = fragmentData.slice(offset, offset + chunkSize);
111
- offset += chunkSize;
112
- this.push(Buffer.from(chunk));
113
- } });
114
- }
115
112
  function convertTimestamp(pts, timebase, timescale) {
116
113
  return Math.round(pts * timescale / timebase.den);
117
114
  }
@@ -214,25 +211,39 @@ var SegmentAccumulator = class {
214
211
  }
215
212
  }
216
213
  };
217
- const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapping) => {
214
+ const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapping, options) => {
218
215
  const parser = new StreamingBoxParser();
219
- const chunks = [];
216
+ const tempFile = (0, node_path.join)(options?.tmpDir ?? (0, node_os.tmpdir)(), `ef-probe-${(0, node_crypto.randomBytes)(8).toString("hex")}.mp4`);
220
217
  let totalSize = 0;
221
- await (0, node_stream_promises.pipeline)(inputStream, parser, new node_stream.Writable({ write(chunk, _encoding, callback) {
222
- chunks.push(chunk);
218
+ const dest = new node_stream.Writable({ write(chunk, _encoding, callback) {
223
219
  totalSize += chunk.length;
224
220
  callback();
225
- } }));
221
+ } });
222
+ const tempWriteStream = (0, node_fs.createWriteStream)(tempFile);
223
+ await (0, node_stream_promises.pipeline)(inputStream, new node_stream.Transform({
224
+ transform(chunk, _encoding, callback) {
225
+ tempWriteStream.write(chunk);
226
+ this.push(chunk);
227
+ callback();
228
+ },
229
+ flush(callback) {
230
+ tempWriteStream.end(() => callback());
231
+ }
232
+ }), parser, dest);
226
233
  const fragments = parser.getFragments();
227
- if (totalSize === 0) return {};
228
- const completeData = Buffer.concat(chunks);
229
- const completeStream = createFragmentStream(new Uint8Array(completeData.buffer, completeData.byteOffset, completeData.byteLength));
234
+ if (totalSize === 0) {
235
+ await (0, node_fs_promises.unlink)(tempFile).catch(() => {});
236
+ return {};
237
+ }
230
238
  let probe;
231
239
  try {
232
- probe = await require_Probe.PacketProbe.probeStream(completeStream);
240
+ probe = await require_Probe.PacketProbe.probePath(tempFile);
233
241
  } catch (error) {
234
242
  console.warn("Failed to probe stream with ffprobe:", error);
243
+ await (0, node_fs_promises.unlink)(tempFile).catch(() => {});
235
244
  return {};
245
+ } finally {
246
+ await (0, node_fs_promises.unlink)(tempFile).catch(() => {});
236
247
  }
237
248
  const videoStreams = probe.videoStreams;
238
249
  const audioStreams = probe.audioStreams;
@@ -340,9 +351,9 @@ const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapp
340
351
  }
341
352
  const timescale = Math.round(timebase.den / timebase.num);
342
353
  const streamPackets = probe.packets.filter((p) => p.stream_index === videoStream.index);
343
- const keyframePackets = streamPackets.filter((p) => p.flags?.includes("K"));
344
- const totalSampleCount = keyframePackets.length;
345
- log(`Complete stream has ${streamPackets.length} video packets, ${keyframePackets.length} keyframes for stream ${videoStream.index}`);
354
+ const keyframeCount = streamPackets.filter((p) => p.flags?.includes("K")).length;
355
+ const totalSampleCount = streamPackets.length;
356
+ log(`Complete stream has ${streamPackets.length} video packets, ${keyframeCount} keyframes for stream ${videoStream.index}`);
346
357
  let trackStartTimeOffsetMs;
347
358
  if (streamPackets.length > 0) {
348
359
  log(`First video packet dts_time: ${streamPackets[0].dts_time}, pts_time: ${streamPackets[0].pts_time}`);
@@ -356,7 +367,9 @@ const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapp
356
367
  const firstPacket = streamPackets[0];
357
368
  const lastPacket = streamPackets[streamPackets.length - 1];
358
369
  const firstPts = convertTimestamp(firstPacket.pts, timebase, timescale);
359
- totalDuration = convertTimestamp(lastPacket.pts, timebase, timescale) - firstPts;
370
+ const lastPts = convertTimestamp(lastPacket.pts, timebase, timescale);
371
+ const lastDuration = convertTimestamp(lastPacket.duration ?? 0, timebase, timescale);
372
+ totalDuration = lastPts - firstPts + lastDuration;
360
373
  }
361
374
  const finalTrackId = trackIdMapping?.[videoStream.index] ?? videoStream.index + 1;
362
375
  trackIndexes[finalTrackId] = {
@@ -1 +1 @@
1
- {"version":3,"file":"generateFragmentIndex.cjs","names":["Transform","box: MP4BoxHeader","Readable","chunks: Buffer[]","Writable","probe: PacketProbe","PacketProbe","trackIndexes: Record<number, TrackFragmentIndex>","fragmentTimingData: FragmentTimingData[]","segments: TrackSegment[]","trackStartTimeOffsetMs: number | undefined"],"sources":["../src/generateFragmentIndex.ts"],"sourcesContent":["import { Readable, Transform, Writable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport debug from \"debug\";\nimport type { TrackFragmentIndex, TrackSegment } from \"./Probe.js\";\nimport { PacketProbe } from \"./Probe.js\";\n\nconst log = debug(\"ef:generateFragmentIndex\");\n\n// Minimum segment duration in milliseconds\nconst MIN_SEGMENT_DURATION_MS = 2000; // 2 seconds\nconst MS_PER_SECOND = 1000;\n\n// ============================================================================\n// Core Domain Types (Type Safety as Invariant Enforcement)\n// ============================================================================\n\n/** Raw packet from ffprobe - the fundamental unit of media data */\ninterface ProbePacket {\n stream_index: number;\n pts: number;\n dts: number;\n pts_time: number;\n dts_time: number;\n duration?: number;\n pos?: number;\n flags?: string;\n}\n\n/** Video packet with keyframe status - invariant: isKeyframe is always defined */\ninterface VideoPacket {\n pts: number;\n dts: number;\n duration?: number;\n isKeyframe: boolean;\n}\n\n/** Audio packet - simpler than video, no keyframe concept */\ninterface AudioPacket {\n pts: number;\n dts: number;\n duration?: number;\n}\n\n/** Fragment timing data - packets organized by fragment */\ninterface FragmentTimingData {\n fragmentIndex: number;\n videoPackets: VideoPacket[];\n audioPackets: AudioPacket[];\n}\n\n/** Timebase for timestamp conversion */\ninterface Timebase {\n num: number;\n den: number;\n}\n\n// Helper function to construct H.264 codec string from profile and level\nfunction constructH264CodecString(\n codecTagString: string,\n profile?: string,\n level?: number,\n): string {\n if (codecTagString !== \"avc1\" || !profile || level === undefined) {\n return codecTagString;\n }\n\n // Map H.264 profile names to profile_idc values\n const profileMap: Record<string, number> = {\n Baseline: 0x42,\n Main: 0x4d,\n High: 0x64,\n \"High 10\": 0x6e,\n \"High 422\": 0x7a,\n \"High 444\": 0xf4,\n };\n\n const profileIdc = profileMap[profile];\n if (!profileIdc) {\n return codecTagString;\n }\n\n // Format: avc1.PPCCLL where PP=profile_idc, CC=constraint_flags, LL=level_idc\n const profileHex = profileIdc.toString(16).padStart(2, \"0\");\n const constraintFlags = \"00\"; // Most common case\n const levelHex = level.toString(16).padStart(2, \"0\");\n\n return `${codecTagString}.${profileHex}${constraintFlags}${levelHex}`;\n}\n\ninterface MP4BoxHeader {\n type: string;\n offset: number;\n size: number;\n headerSize: number;\n}\n\ninterface Fragment {\n type: \"init\" | \"media\";\n offset: number;\n size: number;\n moofOffset?: number;\n mdatOffset?: number;\n}\n\n/**\n * Streaming MP4 box parser that detects box boundaries without loading entire file into memory\n */\nclass StreamingBoxParser extends Transform {\n private buffer = Buffer.alloc(0);\n private globalOffset = 0;\n private fragments: Fragment[] = [];\n private currentMoof: MP4BoxHeader | null = null;\n private initSegmentEnd = 0;\n private foundBoxes: MP4BoxHeader[] = [];\n\n constructor() {\n super({ objectMode: false });\n }\n\n _transform(chunk: Buffer, _encoding: BufferEncoding, callback: () => void) {\n // Append new data to our sliding buffer\n this.buffer = Buffer.concat([this.buffer, chunk]);\n\n // Parse all complete boxes in the current buffer\n this.parseBoxes();\n\n // Pass through the original chunk unchanged\n this.push(chunk);\n callback();\n }\n\n private parseBoxes() {\n let bufferOffset = 0;\n\n while (this.buffer.length - bufferOffset >= 8) {\n const size = this.buffer.readUInt32BE(bufferOffset);\n const type = this.buffer\n .subarray(bufferOffset + 4, bufferOffset + 8)\n .toString(\"ascii\");\n\n // Invalid or incomplete box\n if (size === 0 || size < 8 || this.buffer.length < bufferOffset + size) {\n break;\n }\n\n const box: MP4BoxHeader = {\n type,\n offset: this.globalOffset + bufferOffset,\n size,\n headerSize: 8,\n };\n\n log(`Found box: ${box.type} at offset ${box.offset}, size ${box.size}`);\n this.foundBoxes.push(box);\n this.handleBox(box);\n\n bufferOffset += size;\n }\n\n // Update global offset and trim processed data from buffer\n this.globalOffset += bufferOffset;\n this.buffer = this.buffer.subarray(bufferOffset);\n }\n\n private handleBox(box: MP4BoxHeader) {\n switch (box.type) {\n case \"ftyp\":\n case \"moov\":\n // Part of init segment\n this.initSegmentEnd = Math.max(\n this.initSegmentEnd,\n box.offset + box.size,\n );\n break;\n\n case \"moof\":\n this.currentMoof = box;\n break;\n\n case \"mdat\":\n if (this.currentMoof) {\n // Found a complete fragment (moof + mdat pair) - fragmented MP4\n this.fragments.push({\n type: \"media\",\n offset: this.currentMoof.offset,\n size: box.offset + box.size - this.currentMoof.offset,\n moofOffset: this.currentMoof.offset,\n mdatOffset: box.offset,\n });\n this.currentMoof = null;\n } else {\n // mdat without moof - this is non-fragmented content, not a fragment\n // Common in mixed MP4 files where initial content is non-fragmented\n // followed by fragmented content. Ignore for fragment indexing.\n log(\n `Found non-fragmented mdat at offset ${box.offset}, skipping for fragment index`,\n );\n }\n break;\n }\n }\n\n _flush(callback: () => void) {\n this.parseBoxes(); // Process any remaining buffered data\n\n // Probe always outputs fragmented MP4\n // Init segment is ftyp + moov boxes before the first moof\n if (this.initSegmentEnd > 0) {\n this.fragments.unshift({\n type: \"init\",\n offset: 0,\n size: this.initSegmentEnd,\n });\n }\n\n callback();\n }\n\n getFragments(): Fragment[] {\n return this.fragments;\n }\n}\n\n// Helper function to create a readable stream from fragment data\nfunction createFragmentStream(fragmentData: Uint8Array): Readable {\n let offset = 0;\n return new Readable({\n read() {\n if (offset >= fragmentData.length) {\n this.push(null);\n return;\n }\n\n const chunkSize = Math.min(64 * 1024, fragmentData.length - offset); // 64KB chunks\n const chunk = fragmentData.slice(offset, offset + chunkSize);\n offset += chunkSize;\n this.push(Buffer.from(chunk));\n },\n });\n}\n\n// Helper to convert timestamp from ffprobe timebase to track timescale\nfunction convertTimestamp(\n pts: number,\n timebase: Timebase,\n timescale: number,\n): number {\n return Math.round((pts * timescale) / timebase.den);\n}\n\n// Helper to calculate duration in milliseconds from timescale units\nfunction durationMsFromTimescale(\n durationTimescale: number,\n timescale: number,\n): number {\n return (durationTimescale / timescale) * MS_PER_SECOND;\n}\n\n// Helper to calculate segment byte range from accumulated fragments\nfunction calculateSegmentByteRange(\n accumulatedFragments: Array<{ fragment: Fragment }>,\n): { offset: number; size: number } {\n const firstFrag = accumulatedFragments[0]!;\n const lastFrag = accumulatedFragments[accumulatedFragments.length - 1]!;\n return {\n offset: firstFrag.fragment.offset,\n size:\n lastFrag.fragment.offset +\n lastFrag.fragment.size -\n firstFrag.fragment.offset,\n };\n}\n\n// Explicit enumeration of segment accumulation state (Enumerate the Core Concept)\ntype SegmentAccumulationState =\n | { type: \"idle\" }\n | {\n type: \"accumulating\";\n startPts: number;\n startDts: number;\n fragments: Array<{\n fragment: Fragment;\n fragmentData: FragmentTimingData;\n }>;\n };\n\n// Invariant: Segment must start on keyframe (for video) and have minimum duration\ninterface SegmentEvaluation {\n cts: number;\n dts: number;\n duration: number;\n offset: number;\n size: number;\n}\n\n// Track processing context - single source of truth for track processing\ninterface TrackProcessingContext {\n timebase: Timebase;\n timescale: number;\n fragmentTimingData: FragmentTimingData[];\n mediaFragments: Fragment[];\n // Cached filtered packets for this stream (Performance Through Caching)\n streamPackets: ProbePacket[];\n streamType: \"video\" | \"audio\";\n streamIndex: number;\n}\n\n// Segment accumulator that encapsulates accumulation logic\nclass SegmentAccumulator {\n private state: SegmentAccumulationState = { type: \"idle\" };\n private readonly context: TrackProcessingContext;\n private readonly minDurationMs: number;\n\n constructor(context: TrackProcessingContext, minDurationMs: number) {\n this.context = context;\n this.minDurationMs = minDurationMs;\n }\n\n // Evaluation: Determine if we should finalize (semantics)\n shouldFinalize(nextKeyframe: { pts: number; dts: number } | null): boolean {\n if (this.state.type !== \"accumulating\") {\n return false;\n }\n\n const durationMs = this.calculateAccumulatedDurationMs();\n const hasMinimumDuration = durationMs >= this.minDurationMs;\n\n // For video: finalize on keyframe + minimum duration\n // For audio: finalize on minimum duration (no keyframe requirement)\n if (this.context.streamType === \"video\") {\n return hasMinimumDuration && nextKeyframe !== null;\n } else {\n return hasMinimumDuration;\n }\n }\n\n // Evaluation: Calculate what the segment would be (semantics)\n evaluateSegment(\n nextBoundary: { pts: number } | null,\n ): SegmentEvaluation | null {\n if (this.state.type !== \"accumulating\") {\n return null;\n }\n\n const segmentCts = convertTimestamp(\n this.state.startPts,\n this.context.timebase,\n this.context.timescale,\n );\n const segmentDts = convertTimestamp(\n this.state.startDts,\n this.context.timebase,\n this.context.timescale,\n );\n const segmentDuration = this.calculateSegmentDuration(\n segmentCts,\n nextBoundary,\n );\n const { offset, size } = calculateSegmentByteRange(this.state.fragments);\n\n return {\n cts: segmentCts,\n dts: segmentDts,\n duration: segmentDuration,\n offset,\n size,\n };\n }\n\n // Application: Add fragment to accumulation (mechanism)\n addFragment(fragment: Fragment, fragmentData: FragmentTimingData): void {\n if (this.state.type === \"idle\") {\n // Start accumulation - invariant: video segments must start on keyframe\n const startPts = this.getStartPts(fragmentData);\n const startDts = this.getStartDts(fragmentData);\n this.state = {\n type: \"accumulating\",\n startPts,\n startDts,\n fragments: [{ fragment, fragmentData }],\n };\n } else {\n // Continue accumulation\n this.state.fragments.push({ fragment, fragmentData });\n }\n }\n\n // Application: Reset accumulation (mechanism)\n reset(): void {\n this.state = { type: \"idle\" };\n }\n\n // Application: Start new segment with keyframe (mechanism)\n startNewSegment(keyframe: { pts: number; dts: number }): void {\n this.state = {\n type: \"accumulating\",\n startPts: keyframe.pts,\n startDts: keyframe.dts,\n fragments: [],\n };\n }\n\n // Query: Get current state\n getState(): SegmentAccumulationState {\n return this.state;\n }\n\n // Query: Check if accumulating\n isAccumulating(): boolean {\n return this.state.type === \"accumulating\";\n }\n\n // Private helpers\n private calculateAccumulatedDurationMs(): number {\n if (this.state.type !== \"accumulating\") {\n return 0;\n }\n\n const lastFrag = this.state.fragments[this.state.fragments.length - 1]!;\n const lastPacket = this.getLastPacket(lastFrag.fragmentData);\n const endCts = convertTimestamp(\n lastPacket.pts + (lastPacket.duration || 0),\n this.context.timebase,\n this.context.timescale,\n );\n const startCts = convertTimestamp(\n this.state.startPts,\n this.context.timebase,\n this.context.timescale,\n );\n return durationMsFromTimescale(endCts - startCts, this.context.timescale);\n }\n\n private calculateSegmentDuration(\n segmentCts: number,\n nextBoundary: { pts: number } | null,\n ): number {\n if (nextBoundary) {\n const nextSegmentCts = convertTimestamp(\n nextBoundary.pts,\n this.context.timebase,\n this.context.timescale,\n );\n return nextSegmentCts - segmentCts;\n }\n\n // Last segment: duration to end of all packets\n // Use pre-cached streamPackets (Performance Through Caching)\n const sortedPackets = [...this.context.streamPackets].sort(\n (a, b) => a.pts - b.pts,\n );\n const lastPacket = sortedPackets[sortedPackets.length - 1]!;\n const streamEnd = convertTimestamp(\n lastPacket.pts + (lastPacket.duration || 0),\n this.context.timebase,\n this.context.timescale,\n );\n return streamEnd - segmentCts;\n }\n\n private getStartPts(fragmentData: FragmentTimingData): number {\n if (this.context.streamType === \"video\") {\n const keyframe = fragmentData.videoPackets.find((p) => p.isKeyframe);\n return keyframe?.pts ?? fragmentData.videoPackets[0]?.pts ?? 0;\n } else {\n return fragmentData.audioPackets[0]?.pts ?? 0;\n }\n }\n\n private getStartDts(fragmentData: FragmentTimingData): number {\n if (this.context.streamType === \"video\") {\n const keyframe = fragmentData.videoPackets.find((p) => p.isKeyframe);\n return keyframe?.dts ?? fragmentData.videoPackets[0]?.dts ?? 0;\n } else {\n return fragmentData.audioPackets[0]?.dts ?? 0;\n }\n }\n\n private getLastPacket(fragmentData: FragmentTimingData): {\n pts: number;\n duration?: number;\n } {\n if (this.context.streamType === \"video\") {\n const packets = fragmentData.videoPackets;\n return packets[packets.length - 1]!;\n } else {\n const packets = fragmentData.audioPackets;\n return packets[packets.length - 1]!;\n }\n }\n}\n\n// Helper function to extract fragment data (init + media fragment)\n\nexport const generateFragmentIndex = async (\n inputStream: Readable,\n startTimeOffsetMs?: number,\n trackIdMapping?: Record<number, number>, // Map from source track ID to desired track ID\n): Promise<Record<number, TrackFragmentIndex>> => {\n // Step 1: Create a streaming parser that detects fragment boundaries\n const parser = new StreamingBoxParser();\n\n // Step 2: Create a passthrough stream that doesn't buffer everything\n const chunks: Buffer[] = [];\n let totalSize = 0;\n\n const dest = new Writable({\n write(chunk, _encoding, callback) {\n chunks.push(chunk);\n totalSize += chunk.length;\n callback();\n },\n });\n\n // Process the stream through both parser and collection\n await pipeline(inputStream, parser, dest);\n const fragments = parser.getFragments();\n\n // If no data was collected, return empty result\n if (totalSize === 0) {\n return {};\n }\n\n // Step 3: Use ffprobe to analyze the complete stream for track metadata\n const completeData = Buffer.concat(chunks as readonly Uint8Array[]);\n const completeStream = createFragmentStream(\n new Uint8Array(\n completeData.buffer,\n completeData.byteOffset,\n completeData.byteLength,\n ),\n );\n\n let probe: PacketProbe;\n try {\n probe = await PacketProbe.probeStream(completeStream);\n } catch (error) {\n console.warn(\"Failed to probe stream with ffprobe:\", error);\n return {};\n }\n\n const videoStreams = probe.videoStreams;\n const audioStreams = probe.audioStreams;\n\n const trackIndexes: Record<number, TrackFragmentIndex> = {};\n const initFragment = fragments.find((f) => f.type === \"init\");\n const mediaFragments = fragments.filter((f) => f.type === \"media\");\n\n // Map packets to fragments using byte position for moof+mdat boundaries\n // But create contiguous segments based on keyframes\n const fragmentTimingData: FragmentTimingData[] = [];\n\n for (\n let fragmentIndex = 0;\n fragmentIndex < mediaFragments.length;\n fragmentIndex++\n ) {\n const fragment = mediaFragments[fragmentIndex]!;\n\n // Find packets that belong to this fragment based on byte position (moof+mdat boundaries)\n const fragmentStart = fragment.offset;\n const fragmentEnd = fragment.offset + fragment.size;\n\n const videoPackets = probe.packets\n .filter((packet) => {\n const stream = videoStreams.find(\n (s) => s.index === packet.stream_index,\n );\n return (\n stream?.codec_type === \"video\" &&\n packet.pos !== undefined &&\n packet.pos >= fragmentStart &&\n packet.pos < fragmentEnd\n );\n })\n .map((packet) => ({\n pts: packet.pts,\n dts: packet.dts,\n duration: packet.duration,\n isKeyframe: packet.flags?.includes(\"K\") ?? false,\n }));\n\n const audioPackets = probe.packets\n .filter((packet) => {\n const stream = audioStreams.find(\n (s) => s.index === packet.stream_index,\n );\n return (\n stream?.codec_type === \"audio\" &&\n packet.pos !== undefined &&\n packet.pos >= fragmentStart &&\n packet.pos < fragmentEnd\n );\n })\n .map((packet) => ({\n pts: packet.pts,\n dts: packet.dts,\n duration: packet.duration,\n }));\n\n fragmentTimingData.push({\n fragmentIndex,\n videoPackets,\n audioPackets,\n });\n }\n\n // Unified track processing function (One Direction of Truth)\n const processTrack = (\n streamIndex: number,\n streamType: \"video\" | \"audio\",\n timebase: Timebase,\n allPackets: ProbePacket[],\n ): TrackSegment[] => {\n const segments: TrackSegment[] = [];\n const timescale = Math.round(timebase.den / timebase.num);\n\n // Cache filtered packets once (Performance Through Caching)\n const streamPackets = allPackets.filter(\n (p) => p.stream_index === streamIndex,\n );\n\n const context: TrackProcessingContext = {\n timebase,\n timescale,\n fragmentTimingData,\n mediaFragments,\n streamPackets,\n streamType,\n streamIndex,\n };\n\n const accumulator = new SegmentAccumulator(\n context,\n MIN_SEGMENT_DURATION_MS,\n );\n\n for (let i = 0; i < fragmentTimingData.length; i++) {\n const fragmentData = fragmentTimingData[i]!;\n const fragment = mediaFragments[fragmentData.fragmentIndex]!;\n const packets =\n streamType === \"video\"\n ? fragmentData.videoPackets\n : fragmentData.audioPackets;\n\n log(\n `Fragment ${fragmentData.fragmentIndex}: ${packets.length} ${streamType} packets`,\n );\n\n if (packets.length === 0) {\n log(\n `Skipping fragment ${fragmentData.fragmentIndex} - no ${streamType} packets`,\n );\n continue;\n }\n\n if (streamType === \"video\") {\n // Video: segments must start on keyframes\n const keyframe = fragmentData.videoPackets.find((p) => p.isKeyframe);\n const hasKeyframe = keyframe !== undefined;\n\n // Start new segment on keyframe if none exists\n if (!accumulator.isAccumulating() && hasKeyframe) {\n accumulator.startNewSegment({\n pts: keyframe.pts,\n dts: keyframe.dts,\n });\n accumulator.addFragment(fragment, fragmentData);\n continue;\n }\n\n // Skip fragments without keyframes if no segment started\n if (!accumulator.isAccumulating()) {\n continue;\n }\n\n // Check if we should finalize when encountering a new keyframe\n if (hasKeyframe) {\n if (\n accumulator.shouldFinalize({ pts: keyframe.pts, dts: keyframe.dts })\n ) {\n // Duration should be to the start of this keyframe (start of next segment)\n const nextBoundary = { pts: keyframe.pts };\n const evaluation = accumulator.evaluateSegment(nextBoundary);\n if (evaluation) {\n segments.push(evaluation);\n }\n accumulator.reset();\n accumulator.startNewSegment({\n pts: keyframe.pts,\n dts: keyframe.dts,\n });\n }\n }\n } else {\n // Audio: no keyframe requirement, just duration-based\n if (!accumulator.isAccumulating()) {\n accumulator.addFragment(fragment, fragmentData);\n continue;\n }\n\n // Check if we should finalize based on accumulated duration\n if (accumulator.shouldFinalize(null)) {\n // Duration should be to the start of this fragment (start of next segment)\n const nextBoundary = { pts: fragmentData.audioPackets[0]!.pts };\n const evaluation = accumulator.evaluateSegment(nextBoundary);\n if (evaluation) {\n segments.push(evaluation);\n }\n accumulator.reset();\n }\n }\n\n // Add fragment to current segment\n accumulator.addFragment(fragment, fragmentData);\n }\n\n // Finalize any remaining accumulated fragments\n if (accumulator.isAccumulating()) {\n const evaluation = accumulator.evaluateSegment(null);\n if (evaluation) {\n segments.push(evaluation);\n }\n }\n\n return segments;\n };\n\n // Step 4: Process video tracks using ffprobe data\n for (const videoStream of videoStreams) {\n // Get timebase for this stream to convert timestamps\n const timebase = probe.videoTimebase;\n if (!timebase) {\n console.warn(\"No timebase found for video stream\");\n continue;\n }\n\n const timescale = Math.round(timebase.den / timebase.num);\n\n // Cache filtered packets once (Performance Through Caching)\n const streamPackets = (probe.packets as ProbePacket[]).filter(\n (p) => p.stream_index === videoStream.index,\n );\n const keyframePackets = streamPackets.filter((p) => p.flags?.includes(\"K\"));\n const totalSampleCount = keyframePackets.length;\n\n log(\n `Complete stream has ${streamPackets.length} video packets, ${keyframePackets.length} keyframes for stream ${videoStream.index}`,\n );\n\n // Calculate per-track timing offset from first packet for timeline mapping\n let trackStartTimeOffsetMs: number | undefined;\n if (streamPackets.length > 0) {\n log(\n `First video packet dts_time: ${streamPackets[0]!.dts_time}, pts_time: ${streamPackets[0]!.pts_time}`,\n );\n const presentationTime = streamPackets[0]!.pts_time;\n if (Math.abs(presentationTime) > 0.01) {\n trackStartTimeOffsetMs = presentationTime * MS_PER_SECOND;\n }\n }\n if (startTimeOffsetMs !== undefined) {\n trackStartTimeOffsetMs = startTimeOffsetMs;\n }\n\n // Process fragments to create segments with minimum duration\n const segments = processTrack(\n videoStream.index,\n \"video\",\n timebase,\n probe.packets as ProbePacket[],\n );\n\n // Calculate total duration from cached stream packets\n let totalDuration = 0;\n if (streamPackets.length > 0) {\n const firstPacket = streamPackets[0]!;\n const lastPacket = streamPackets[streamPackets.length - 1]!;\n const firstPts = convertTimestamp(firstPacket.pts, timebase, timescale);\n const lastPts = convertTimestamp(lastPacket.pts, timebase, timescale);\n totalDuration = lastPts - firstPts;\n }\n\n const finalTrackId =\n trackIdMapping?.[videoStream.index] ?? videoStream.index + 1;\n trackIndexes[finalTrackId] = {\n track: finalTrackId,\n type: \"video\",\n width: videoStream.coded_width || videoStream.width,\n height: videoStream.coded_height || videoStream.height,\n timescale: timescale,\n sample_count: totalSampleCount,\n codec: constructH264CodecString(\n videoStream.codec_tag_string,\n videoStream.profile,\n videoStream.level,\n ),\n duration: totalDuration,\n startTimeOffsetMs: trackStartTimeOffsetMs,\n initSegment: {\n offset: 0,\n size: initFragment?.size || 0,\n },\n segments,\n };\n }\n\n // Step 5: Process audio tracks using ffprobe data\n for (const audioStream of audioStreams) {\n // Get timebase for this stream to convert timestamps\n const timebase = probe.audioTimebase;\n if (!timebase) {\n console.warn(\"No timebase found for audio stream\");\n continue;\n }\n\n const timescale = Math.round(timebase.den / timebase.num);\n\n // Cache filtered packets once (Performance Through Caching)\n const streamPackets = (probe.packets as ProbePacket[]).filter(\n (p) => p.stream_index === audioStream.index,\n );\n const totalSampleCount = streamPackets.length;\n\n // Calculate per-track timing offset from first packet for timeline mapping\n let trackStartTimeOffsetMs: number | undefined;\n if (streamPackets.length > 0) {\n const presentationTime = streamPackets[0]!.pts_time;\n if (Math.abs(presentationTime) > 0.01) {\n trackStartTimeOffsetMs = presentationTime * MS_PER_SECOND;\n }\n }\n if (startTimeOffsetMs !== undefined) {\n trackStartTimeOffsetMs = startTimeOffsetMs;\n }\n\n // Process fragments to create segments with minimum duration\n const segments = processTrack(\n audioStream.index,\n \"audio\",\n timebase,\n probe.packets as ProbePacket[],\n );\n\n // Calculate total duration\n const totalDuration = segments.reduce((sum, seg) => sum + seg.duration, 0);\n\n const finalTrackId =\n trackIdMapping?.[audioStream.index] ?? audioStream.index + 1;\n trackIndexes[finalTrackId] = {\n track: finalTrackId,\n type: \"audio\",\n channel_count: audioStream.channels,\n sample_rate: Number(audioStream.sample_rate),\n sample_size: audioStream.bits_per_sample,\n sample_count: totalSampleCount,\n timescale: timescale,\n codec: audioStream.codec_tag_string || audioStream.codec_name || \"\",\n duration: totalDuration,\n startTimeOffsetMs: trackStartTimeOffsetMs,\n initSegment: {\n offset: 0,\n size: initFragment?.size || 0,\n },\n segments,\n };\n }\n\n return trackIndexes;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"generateFragmentIndex.cjs","names":["Transform","box: MP4BoxHeader","Writable","probe: PacketProbe","PacketProbe","trackIndexes: Record<number, TrackFragmentIndex>","fragmentTimingData: FragmentTimingData[]","segments: TrackSegment[]","trackStartTimeOffsetMs: number | undefined"],"sources":["../src/generateFragmentIndex.ts"],"sourcesContent":["import { Readable, Transform, Writable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { createWriteStream } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { randomBytes } from \"node:crypto\";\nimport debug from \"debug\";\nimport type { TrackFragmentIndex, TrackSegment } from \"./Probe.js\";\nimport { PacketProbe } from \"./Probe.js\";\n\nconst log = debug(\"ef:generateFragmentIndex\");\n\n// Minimum segment duration in milliseconds\nconst MIN_SEGMENT_DURATION_MS = 2000; // 2 seconds\nconst MS_PER_SECOND = 1000;\n\n// ============================================================================\n// Core Domain Types (Type Safety as Invariant Enforcement)\n// ============================================================================\n\n/** Raw packet from ffprobe - the fundamental unit of media data */\ninterface ProbePacket {\n stream_index: number;\n pts: number;\n dts: number;\n pts_time: number;\n dts_time: number;\n duration?: number;\n pos?: number;\n flags?: string;\n}\n\n/** Video packet with keyframe status - invariant: isKeyframe is always defined */\ninterface VideoPacket {\n pts: number;\n dts: number;\n duration?: number;\n isKeyframe: boolean;\n}\n\n/** Audio packet - simpler than video, no keyframe concept */\ninterface AudioPacket {\n pts: number;\n dts: number;\n duration?: number;\n}\n\n/** Fragment timing data - packets organized by fragment */\ninterface FragmentTimingData {\n fragmentIndex: number;\n videoPackets: VideoPacket[];\n audioPackets: AudioPacket[];\n}\n\n/** Timebase for timestamp conversion */\ninterface Timebase {\n num: number;\n den: number;\n}\n\n// Helper function to construct H.264 codec string from profile and level\nfunction constructH264CodecString(\n codecTagString: string,\n profile?: string,\n level?: number,\n): string {\n if (codecTagString !== \"avc1\" || !profile || level === undefined) {\n return codecTagString;\n }\n\n // Map H.264 profile names to profile_idc values\n const profileMap: Record<string, number> = {\n Baseline: 0x42,\n Main: 0x4d,\n High: 0x64,\n \"High 10\": 0x6e,\n \"High 422\": 0x7a,\n \"High 444\": 0xf4,\n };\n\n const profileIdc = profileMap[profile];\n if (!profileIdc) {\n return codecTagString;\n }\n\n // Format: avc1.PPCCLL where PP=profile_idc, CC=constraint_flags, LL=level_idc\n const profileHex = profileIdc.toString(16).padStart(2, \"0\");\n const constraintFlags = \"00\"; // Most common case\n const levelHex = level.toString(16).padStart(2, \"0\");\n\n return `${codecTagString}.${profileHex}${constraintFlags}${levelHex}`;\n}\n\ninterface MP4BoxHeader {\n type: string;\n offset: number;\n size: number;\n headerSize: number;\n}\n\ninterface Fragment {\n type: \"init\" | \"media\";\n offset: number;\n size: number;\n moofOffset?: number;\n mdatOffset?: number;\n}\n\n/**\n * Streaming MP4 box parser that detects box boundaries without loading entire file into memory\n */\nclass StreamingBoxParser extends Transform {\n private buffer = Buffer.alloc(0);\n private globalOffset = 0;\n private fragments: Fragment[] = [];\n private currentMoof: MP4BoxHeader | null = null;\n private initSegmentEnd = 0;\n private foundBoxes: MP4BoxHeader[] = [];\n\n constructor() {\n super({ objectMode: false });\n }\n\n _transform(chunk: Buffer, _encoding: BufferEncoding, callback: () => void) {\n // Append new data to our sliding buffer\n this.buffer = Buffer.concat([this.buffer, chunk]);\n\n // Parse all complete boxes in the current buffer\n this.parseBoxes();\n\n // Pass through the original chunk unchanged\n this.push(chunk);\n callback();\n }\n\n private parseBoxes() {\n let bufferOffset = 0;\n\n while (this.buffer.length - bufferOffset >= 8) {\n const size = this.buffer.readUInt32BE(bufferOffset);\n const type = this.buffer\n .subarray(bufferOffset + 4, bufferOffset + 8)\n .toString(\"ascii\");\n\n // Invalid or incomplete box\n if (size === 0 || size < 8 || this.buffer.length < bufferOffset + size) {\n break;\n }\n\n const box: MP4BoxHeader = {\n type,\n offset: this.globalOffset + bufferOffset,\n size,\n headerSize: 8,\n };\n\n log(`Found box: ${box.type} at offset ${box.offset}, size ${box.size}`);\n this.foundBoxes.push(box);\n this.handleBox(box);\n\n bufferOffset += size;\n }\n\n // Update global offset and trim processed data from buffer\n this.globalOffset += bufferOffset;\n this.buffer = this.buffer.subarray(bufferOffset);\n }\n\n private handleBox(box: MP4BoxHeader) {\n switch (box.type) {\n case \"ftyp\":\n case \"moov\":\n // Part of init segment\n this.initSegmentEnd = Math.max(\n this.initSegmentEnd,\n box.offset + box.size,\n );\n break;\n\n case \"moof\":\n this.currentMoof = box;\n break;\n\n case \"mdat\":\n if (this.currentMoof) {\n // Found a complete fragment (moof + mdat pair) - fragmented MP4\n this.fragments.push({\n type: \"media\",\n offset: this.currentMoof.offset,\n size: box.offset + box.size - this.currentMoof.offset,\n moofOffset: this.currentMoof.offset,\n mdatOffset: box.offset,\n });\n this.currentMoof = null;\n } else {\n // mdat without moof - this is non-fragmented content, not a fragment\n // Common in mixed MP4 files where initial content is non-fragmented\n // followed by fragmented content. Ignore for fragment indexing.\n log(\n `Found non-fragmented mdat at offset ${box.offset}, skipping for fragment index`,\n );\n }\n break;\n }\n }\n\n _flush(callback: () => void) {\n this.parseBoxes(); // Process any remaining buffered data\n\n // Probe always outputs fragmented MP4\n // Init segment is ftyp + moov boxes before the first moof\n if (this.initSegmentEnd > 0) {\n this.fragments.unshift({\n type: \"init\",\n offset: 0,\n size: this.initSegmentEnd,\n });\n }\n\n callback();\n }\n\n getFragments(): Fragment[] {\n return this.fragments;\n }\n}\n\n// Helper to convert timestamp from ffprobe timebase to track timescale\nfunction convertTimestamp(\n pts: number,\n timebase: Timebase,\n timescale: number,\n): number {\n return Math.round((pts * timescale) / timebase.den);\n}\n\n// Helper to calculate duration in milliseconds from timescale units\nfunction durationMsFromTimescale(\n durationTimescale: number,\n timescale: number,\n): number {\n return (durationTimescale / timescale) * MS_PER_SECOND;\n}\n\n// Helper to calculate segment byte range from accumulated fragments\nfunction calculateSegmentByteRange(\n accumulatedFragments: Array<{ fragment: Fragment }>,\n): { offset: number; size: number } {\n const firstFrag = accumulatedFragments[0]!;\n const lastFrag = accumulatedFragments[accumulatedFragments.length - 1]!;\n return {\n offset: firstFrag.fragment.offset,\n size:\n lastFrag.fragment.offset +\n lastFrag.fragment.size -\n firstFrag.fragment.offset,\n };\n}\n\n// Explicit enumeration of segment accumulation state (Enumerate the Core Concept)\ntype SegmentAccumulationState =\n | { type: \"idle\" }\n | {\n type: \"accumulating\";\n startPts: number;\n startDts: number;\n fragments: Array<{\n fragment: Fragment;\n fragmentData: FragmentTimingData;\n }>;\n };\n\n// Invariant: Segment must start on keyframe (for video) and have minimum duration\ninterface SegmentEvaluation {\n cts: number;\n dts: number;\n duration: number;\n offset: number;\n size: number;\n}\n\n// Track processing context - single source of truth for track processing\ninterface TrackProcessingContext {\n timebase: Timebase;\n timescale: number;\n fragmentTimingData: FragmentTimingData[];\n mediaFragments: Fragment[];\n // Cached filtered packets for this stream (Performance Through Caching)\n streamPackets: ProbePacket[];\n streamType: \"video\" | \"audio\";\n streamIndex: number;\n}\n\n// Segment accumulator that encapsulates accumulation logic\nclass SegmentAccumulator {\n private state: SegmentAccumulationState = { type: \"idle\" };\n private readonly context: TrackProcessingContext;\n private readonly minDurationMs: number;\n\n constructor(context: TrackProcessingContext, minDurationMs: number) {\n this.context = context;\n this.minDurationMs = minDurationMs;\n }\n\n // Evaluation: Determine if we should finalize (semantics)\n shouldFinalize(nextKeyframe: { pts: number; dts: number } | null): boolean {\n if (this.state.type !== \"accumulating\") {\n return false;\n }\n\n const durationMs = this.calculateAccumulatedDurationMs();\n const hasMinimumDuration = durationMs >= this.minDurationMs;\n\n // For video: finalize on keyframe + minimum duration\n // For audio: finalize on minimum duration (no keyframe requirement)\n if (this.context.streamType === \"video\") {\n return hasMinimumDuration && nextKeyframe !== null;\n } else {\n return hasMinimumDuration;\n }\n }\n\n // Evaluation: Calculate what the segment would be (semantics)\n evaluateSegment(\n nextBoundary: { pts: number } | null,\n ): SegmentEvaluation | null {\n if (this.state.type !== \"accumulating\") {\n return null;\n }\n\n const segmentCts = convertTimestamp(\n this.state.startPts,\n this.context.timebase,\n this.context.timescale,\n );\n const segmentDts = convertTimestamp(\n this.state.startDts,\n this.context.timebase,\n this.context.timescale,\n );\n const segmentDuration = this.calculateSegmentDuration(\n segmentCts,\n nextBoundary,\n );\n const { offset, size } = calculateSegmentByteRange(this.state.fragments);\n\n return {\n cts: segmentCts,\n dts: segmentDts,\n duration: segmentDuration,\n offset,\n size,\n };\n }\n\n // Application: Add fragment to accumulation (mechanism)\n addFragment(fragment: Fragment, fragmentData: FragmentTimingData): void {\n if (this.state.type === \"idle\") {\n // Start accumulation - invariant: video segments must start on keyframe\n const startPts = this.getStartPts(fragmentData);\n const startDts = this.getStartDts(fragmentData);\n this.state = {\n type: \"accumulating\",\n startPts,\n startDts,\n fragments: [{ fragment, fragmentData }],\n };\n } else {\n // Continue accumulation\n this.state.fragments.push({ fragment, fragmentData });\n }\n }\n\n // Application: Reset accumulation (mechanism)\n reset(): void {\n this.state = { type: \"idle\" };\n }\n\n // Application: Start new segment with keyframe (mechanism)\n startNewSegment(keyframe: { pts: number; dts: number }): void {\n this.state = {\n type: \"accumulating\",\n startPts: keyframe.pts,\n startDts: keyframe.dts,\n fragments: [],\n };\n }\n\n // Query: Get current state\n getState(): SegmentAccumulationState {\n return this.state;\n }\n\n // Query: Check if accumulating\n isAccumulating(): boolean {\n return this.state.type === \"accumulating\";\n }\n\n // Private helpers\n private calculateAccumulatedDurationMs(): number {\n if (this.state.type !== \"accumulating\") {\n return 0;\n }\n\n const lastFrag = this.state.fragments[this.state.fragments.length - 1]!;\n const lastPacket = this.getLastPacket(lastFrag.fragmentData);\n const endCts = convertTimestamp(\n lastPacket.pts + (lastPacket.duration || 0),\n this.context.timebase,\n this.context.timescale,\n );\n const startCts = convertTimestamp(\n this.state.startPts,\n this.context.timebase,\n this.context.timescale,\n );\n return durationMsFromTimescale(endCts - startCts, this.context.timescale);\n }\n\n private calculateSegmentDuration(\n segmentCts: number,\n nextBoundary: { pts: number } | null,\n ): number {\n if (nextBoundary) {\n const nextSegmentCts = convertTimestamp(\n nextBoundary.pts,\n this.context.timebase,\n this.context.timescale,\n );\n return nextSegmentCts - segmentCts;\n }\n\n // Last segment: duration to end of all packets\n // Use pre-cached streamPackets (Performance Through Caching)\n const sortedPackets = [...this.context.streamPackets].sort(\n (a, b) => a.pts - b.pts,\n );\n const lastPacket = sortedPackets[sortedPackets.length - 1]!;\n const streamEnd = convertTimestamp(\n lastPacket.pts + (lastPacket.duration || 0),\n this.context.timebase,\n this.context.timescale,\n );\n return streamEnd - segmentCts;\n }\n\n private getStartPts(fragmentData: FragmentTimingData): number {\n if (this.context.streamType === \"video\") {\n const keyframe = fragmentData.videoPackets.find((p) => p.isKeyframe);\n return keyframe?.pts ?? fragmentData.videoPackets[0]?.pts ?? 0;\n } else {\n return fragmentData.audioPackets[0]?.pts ?? 0;\n }\n }\n\n private getStartDts(fragmentData: FragmentTimingData): number {\n if (this.context.streamType === \"video\") {\n const keyframe = fragmentData.videoPackets.find((p) => p.isKeyframe);\n return keyframe?.dts ?? fragmentData.videoPackets[0]?.dts ?? 0;\n } else {\n return fragmentData.audioPackets[0]?.dts ?? 0;\n }\n }\n\n private getLastPacket(fragmentData: FragmentTimingData): {\n pts: number;\n duration?: number;\n } {\n if (this.context.streamType === \"video\") {\n const packets = fragmentData.videoPackets;\n return packets[packets.length - 1]!;\n } else {\n const packets = fragmentData.audioPackets;\n return packets[packets.length - 1]!;\n }\n }\n}\n\n// Helper function to extract fragment data (init + media fragment)\n\nexport const generateFragmentIndex = async (\n inputStream: Readable,\n startTimeOffsetMs?: number,\n trackIdMapping?: Record<number, number>, // Map from source track ID to desired track ID\n options?: { tmpDir?: string },\n): Promise<Record<number, TrackFragmentIndex>> => {\n // Step 1: Create a streaming parser that detects fragment boundaries\n const parser = new StreamingBoxParser();\n\n // Step 2: Write stream to a temp file to avoid buffering the entire MP4 in memory\n const tempDir = options?.tmpDir ?? tmpdir();\n const tempFile = join(\n tempDir,\n `ef-probe-${randomBytes(8).toString(\"hex\")}.mp4`,\n );\n let totalSize = 0;\n\n const dest = new Writable({\n write(chunk, _encoding, callback) {\n totalSize += chunk.length;\n callback();\n },\n });\n\n const tempWriteStream = createWriteStream(tempFile);\n\n // Split input through both parser (for fragment detection) and temp file (for probing)\n // We must tee the stream: pipe inputStream → parser → dest, and also write to tempFile\n const teeTransform = new Transform({\n transform(chunk, _encoding, callback) {\n tempWriteStream.write(chunk);\n this.push(chunk);\n callback();\n },\n flush(callback) {\n tempWriteStream.end(() => callback());\n },\n });\n\n // Process the stream through both parser and collection\n await pipeline(inputStream, teeTransform, parser, dest);\n const fragments = parser.getFragments();\n\n // If no data was collected, clean up and return empty result\n if (totalSize === 0) {\n await unlink(tempFile).catch(() => {});\n return {};\n }\n\n // Step 3: Use ffprobe to analyze the temp file for track metadata (avoids in-memory buffering)\n let probe: PacketProbe;\n try {\n probe = await PacketProbe.probePath(tempFile);\n } catch (error) {\n console.warn(\"Failed to probe stream with ffprobe:\", error);\n await unlink(tempFile).catch(() => {});\n return {};\n } finally {\n await unlink(tempFile).catch(() => {});\n }\n\n const videoStreams = probe.videoStreams;\n const audioStreams = probe.audioStreams;\n\n const trackIndexes: Record<number, TrackFragmentIndex> = {};\n const initFragment = fragments.find((f) => f.type === \"init\");\n const mediaFragments = fragments.filter((f) => f.type === \"media\");\n\n // Map packets to fragments using byte position for moof+mdat boundaries\n // But create contiguous segments based on keyframes\n const fragmentTimingData: FragmentTimingData[] = [];\n\n for (\n let fragmentIndex = 0;\n fragmentIndex < mediaFragments.length;\n fragmentIndex++\n ) {\n const fragment = mediaFragments[fragmentIndex]!;\n\n // Find packets that belong to this fragment based on byte position (moof+mdat boundaries)\n const fragmentStart = fragment.offset;\n const fragmentEnd = fragment.offset + fragment.size;\n\n const videoPackets = probe.packets\n .filter((packet) => {\n const stream = videoStreams.find(\n (s) => s.index === packet.stream_index,\n );\n return (\n stream?.codec_type === \"video\" &&\n packet.pos !== undefined &&\n packet.pos >= fragmentStart &&\n packet.pos < fragmentEnd\n );\n })\n .map((packet) => ({\n pts: packet.pts,\n dts: packet.dts,\n duration: packet.duration,\n isKeyframe: packet.flags?.includes(\"K\") ?? false,\n }));\n\n const audioPackets = probe.packets\n .filter((packet) => {\n const stream = audioStreams.find(\n (s) => s.index === packet.stream_index,\n );\n return (\n stream?.codec_type === \"audio\" &&\n packet.pos !== undefined &&\n packet.pos >= fragmentStart &&\n packet.pos < fragmentEnd\n );\n })\n .map((packet) => ({\n pts: packet.pts,\n dts: packet.dts,\n duration: packet.duration,\n }));\n\n fragmentTimingData.push({\n fragmentIndex,\n videoPackets,\n audioPackets,\n });\n }\n\n // Unified track processing function (One Direction of Truth)\n const processTrack = (\n streamIndex: number,\n streamType: \"video\" | \"audio\",\n timebase: Timebase,\n allPackets: ProbePacket[],\n ): TrackSegment[] => {\n const segments: TrackSegment[] = [];\n const timescale = Math.round(timebase.den / timebase.num);\n\n // Cache filtered packets once (Performance Through Caching)\n const streamPackets = allPackets.filter(\n (p) => p.stream_index === streamIndex,\n );\n\n const context: TrackProcessingContext = {\n timebase,\n timescale,\n fragmentTimingData,\n mediaFragments,\n streamPackets,\n streamType,\n streamIndex,\n };\n\n const accumulator = new SegmentAccumulator(\n context,\n MIN_SEGMENT_DURATION_MS,\n );\n\n for (let i = 0; i < fragmentTimingData.length; i++) {\n const fragmentData = fragmentTimingData[i]!;\n const fragment = mediaFragments[fragmentData.fragmentIndex]!;\n const packets =\n streamType === \"video\"\n ? fragmentData.videoPackets\n : fragmentData.audioPackets;\n\n log(\n `Fragment ${fragmentData.fragmentIndex}: ${packets.length} ${streamType} packets`,\n );\n\n if (packets.length === 0) {\n log(\n `Skipping fragment ${fragmentData.fragmentIndex} - no ${streamType} packets`,\n );\n continue;\n }\n\n if (streamType === \"video\") {\n // Video: segments must start on keyframes\n const keyframe = fragmentData.videoPackets.find((p) => p.isKeyframe);\n const hasKeyframe = keyframe !== undefined;\n\n // Start new segment on keyframe if none exists\n if (!accumulator.isAccumulating() && hasKeyframe) {\n accumulator.startNewSegment({\n pts: keyframe.pts,\n dts: keyframe.dts,\n });\n accumulator.addFragment(fragment, fragmentData);\n continue;\n }\n\n // Skip fragments without keyframes if no segment started\n if (!accumulator.isAccumulating()) {\n continue;\n }\n\n // Check if we should finalize when encountering a new keyframe\n if (hasKeyframe) {\n if (\n accumulator.shouldFinalize({ pts: keyframe.pts, dts: keyframe.dts })\n ) {\n // Duration should be to the start of this keyframe (start of next segment)\n const nextBoundary = { pts: keyframe.pts };\n const evaluation = accumulator.evaluateSegment(nextBoundary);\n if (evaluation) {\n segments.push(evaluation);\n }\n accumulator.reset();\n accumulator.startNewSegment({\n pts: keyframe.pts,\n dts: keyframe.dts,\n });\n }\n }\n } else {\n // Audio: no keyframe requirement, just duration-based\n if (!accumulator.isAccumulating()) {\n accumulator.addFragment(fragment, fragmentData);\n continue;\n }\n\n // Check if we should finalize based on accumulated duration\n if (accumulator.shouldFinalize(null)) {\n // Duration should be to the start of this fragment (start of next segment)\n const nextBoundary = { pts: fragmentData.audioPackets[0]!.pts };\n const evaluation = accumulator.evaluateSegment(nextBoundary);\n if (evaluation) {\n segments.push(evaluation);\n }\n accumulator.reset();\n }\n }\n\n // Add fragment to current segment\n accumulator.addFragment(fragment, fragmentData);\n }\n\n // Finalize any remaining accumulated fragments\n if (accumulator.isAccumulating()) {\n const evaluation = accumulator.evaluateSegment(null);\n if (evaluation) {\n segments.push(evaluation);\n }\n }\n\n return segments;\n };\n\n // Step 4: Process video tracks using ffprobe data\n for (const videoStream of videoStreams) {\n // Get timebase for this stream to convert timestamps\n const timebase = probe.videoTimebase;\n if (!timebase) {\n console.warn(\"No timebase found for video stream\");\n continue;\n }\n\n const timescale = Math.round(timebase.den / timebase.num);\n\n // Cache filtered packets once (Performance Through Caching)\n const streamPackets = (probe.packets as ProbePacket[]).filter(\n (p) => p.stream_index === videoStream.index,\n );\n const keyframeCount = streamPackets.filter((p) =>\n p.flags?.includes(\"K\"),\n ).length;\n const totalSampleCount = streamPackets.length;\n\n log(\n `Complete stream has ${streamPackets.length} video packets, ${keyframeCount} keyframes for stream ${videoStream.index}`,\n );\n\n // Calculate per-track timing offset from first packet for timeline mapping\n let trackStartTimeOffsetMs: number | undefined;\n if (streamPackets.length > 0) {\n log(\n `First video packet dts_time: ${streamPackets[0]!.dts_time}, pts_time: ${streamPackets[0]!.pts_time}`,\n );\n const presentationTime = streamPackets[0]!.pts_time;\n if (Math.abs(presentationTime) > 0.01) {\n trackStartTimeOffsetMs = presentationTime * MS_PER_SECOND;\n }\n }\n if (startTimeOffsetMs !== undefined) {\n trackStartTimeOffsetMs = startTimeOffsetMs;\n }\n\n // Process fragments to create segments with minimum duration\n const segments = processTrack(\n videoStream.index,\n \"video\",\n timebase,\n probe.packets as ProbePacket[],\n );\n\n // Calculate total duration from cached stream packets (inclusive of last frame duration)\n let totalDuration = 0;\n if (streamPackets.length > 0) {\n const firstPacket = streamPackets[0]!;\n const lastPacket = streamPackets[streamPackets.length - 1]!;\n const firstPts = convertTimestamp(firstPacket.pts, timebase, timescale);\n const lastPts = convertTimestamp(lastPacket.pts, timebase, timescale);\n const lastDuration = convertTimestamp(\n lastPacket.duration ?? 0,\n timebase,\n timescale,\n );\n totalDuration = lastPts - firstPts + lastDuration;\n }\n\n const finalTrackId =\n trackIdMapping?.[videoStream.index] ?? videoStream.index + 1;\n trackIndexes[finalTrackId] = {\n track: finalTrackId,\n type: \"video\",\n width: videoStream.coded_width || videoStream.width,\n height: videoStream.coded_height || videoStream.height,\n timescale: timescale,\n sample_count: totalSampleCount,\n codec: constructH264CodecString(\n videoStream.codec_tag_string,\n videoStream.profile,\n videoStream.level,\n ),\n duration: totalDuration,\n startTimeOffsetMs: trackStartTimeOffsetMs,\n initSegment: {\n offset: 0,\n size: initFragment?.size || 0,\n },\n segments,\n };\n }\n\n // Step 5: Process audio tracks using ffprobe data\n for (const audioStream of audioStreams) {\n // Get timebase for this stream to convert timestamps\n const timebase = probe.audioTimebase;\n if (!timebase) {\n console.warn(\"No timebase found for audio stream\");\n continue;\n }\n\n const timescale = Math.round(timebase.den / timebase.num);\n\n // Cache filtered packets once (Performance Through Caching)\n const streamPackets = (probe.packets as ProbePacket[]).filter(\n (p) => p.stream_index === audioStream.index,\n );\n const totalSampleCount = streamPackets.length;\n\n // Calculate per-track timing offset from first packet for timeline mapping\n let trackStartTimeOffsetMs: number | undefined;\n if (streamPackets.length > 0) {\n const presentationTime = streamPackets[0]!.pts_time;\n if (Math.abs(presentationTime) > 0.01) {\n trackStartTimeOffsetMs = presentationTime * MS_PER_SECOND;\n }\n }\n if (startTimeOffsetMs !== undefined) {\n trackStartTimeOffsetMs = startTimeOffsetMs;\n }\n\n // Process fragments to create segments with minimum duration\n const segments = processTrack(\n audioStream.index,\n \"audio\",\n timebase,\n probe.packets as ProbePacket[],\n );\n\n // Calculate total duration\n const totalDuration = segments.reduce((sum, seg) => sum + seg.duration, 0);\n\n const finalTrackId =\n trackIdMapping?.[audioStream.index] ?? audioStream.index + 1;\n trackIndexes[finalTrackId] = {\n track: finalTrackId,\n type: \"audio\",\n channel_count: audioStream.channels,\n sample_rate: Number(audioStream.sample_rate),\n sample_size: audioStream.bits_per_sample,\n sample_count: totalSampleCount,\n timescale: timescale,\n codec: audioStream.codec_tag_string || audioStream.codec_name || \"\",\n duration: totalDuration,\n startTimeOffsetMs: trackStartTimeOffsetMs,\n initSegment: {\n offset: 0,\n size: initFragment?.size || 0,\n },\n segments,\n };\n }\n\n return trackIndexes;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAWA,MAAM,yBAAY,2BAA2B;AAG7C,MAAM,0BAA0B;AAChC,MAAM,gBAAgB;AA+CtB,SAAS,yBACP,gBACA,SACA,OACQ;AACR,KAAI,mBAAmB,UAAU,CAAC,WAAW,UAAU,OACrD,QAAO;CAaT,MAAM,aATqC;EACzC,UAAU;EACV,MAAM;EACN,MAAM;EACN,WAAW;EACX,YAAY;EACZ,YAAY;EACb,CAE6B;AAC9B,KAAI,CAAC,WACH,QAAO;AAQT,QAAO,GAAG,eAAe,GAJN,WAAW,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI,KAE1C,MAAM,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI;;;;;AAuBtD,IAAM,qBAAN,cAAiCA,sBAAU;CAQzC,cAAc;AACZ,QAAM,EAAE,YAAY,OAAO,CAAC;gBARb,OAAO,MAAM,EAAE;sBACT;mBACS,EAAE;qBACS;wBAClB;oBACY,EAAE;;CAMvC,WAAW,OAAe,WAA2B,UAAsB;AAEzE,OAAK,SAAS,OAAO,OAAO,CAAC,KAAK,QAAQ,MAAM,CAAC;AAGjD,OAAK,YAAY;AAGjB,OAAK,KAAK,MAAM;AAChB,YAAU;;CAGZ,AAAQ,aAAa;EACnB,IAAI,eAAe;AAEnB,SAAO,KAAK,OAAO,SAAS,gBAAgB,GAAG;GAC7C,MAAM,OAAO,KAAK,OAAO,aAAa,aAAa;GACnD,MAAM,OAAO,KAAK,OACf,SAAS,eAAe,GAAG,eAAe,EAAE,CAC5C,SAAS,QAAQ;AAGpB,OAAI,SAAS,KAAK,OAAO,KAAK,KAAK,OAAO,SAAS,eAAe,KAChE;GAGF,MAAMC,MAAoB;IACxB;IACA,QAAQ,KAAK,eAAe;IAC5B;IACA,YAAY;IACb;AAED,OAAI,cAAc,IAAI,KAAK,aAAa,IAAI,OAAO,SAAS,IAAI,OAAO;AACvE,QAAK,WAAW,KAAK,IAAI;AACzB,QAAK,UAAU,IAAI;AAEnB,mBAAgB;;AAIlB,OAAK,gBAAgB;AACrB,OAAK,SAAS,KAAK,OAAO,SAAS,aAAa;;CAGlD,AAAQ,UAAU,KAAmB;AACnC,UAAQ,IAAI,MAAZ;GACE,KAAK;GACL,KAAK;AAEH,SAAK,iBAAiB,KAAK,IACzB,KAAK,gBACL,IAAI,SAAS,IAAI,KAClB;AACD;GAEF,KAAK;AACH,SAAK,cAAc;AACnB;GAEF,KAAK;AACH,QAAI,KAAK,aAAa;AAEpB,UAAK,UAAU,KAAK;MAClB,MAAM;MACN,QAAQ,KAAK,YAAY;MACzB,MAAM,IAAI,SAAS,IAAI,OAAO,KAAK,YAAY;MAC/C,YAAY,KAAK,YAAY;MAC7B,YAAY,IAAI;MACjB,CAAC;AACF,UAAK,cAAc;UAKnB,KACE,uCAAuC,IAAI,OAAO,+BACnD;AAEH;;;CAIN,OAAO,UAAsB;AAC3B,OAAK,YAAY;AAIjB,MAAI,KAAK,iBAAiB,EACxB,MAAK,UAAU,QAAQ;GACrB,MAAM;GACN,QAAQ;GACR,MAAM,KAAK;GACZ,CAAC;AAGJ,YAAU;;CAGZ,eAA2B;AACzB,SAAO,KAAK;;;AAKhB,SAAS,iBACP,KACA,UACA,WACQ;AACR,QAAO,KAAK,MAAO,MAAM,YAAa,SAAS,IAAI;;AAIrD,SAAS,wBACP,mBACA,WACQ;AACR,QAAQ,oBAAoB,YAAa;;AAI3C,SAAS,0BACP,sBACkC;CAClC,MAAM,YAAY,qBAAqB;CACvC,MAAM,WAAW,qBAAqB,qBAAqB,SAAS;AACpE,QAAO;EACL,QAAQ,UAAU,SAAS;EAC3B,MACE,SAAS,SAAS,SAClB,SAAS,SAAS,OAClB,UAAU,SAAS;EACtB;;AAsCH,IAAM,qBAAN,MAAyB;CAKvB,YAAY,SAAiC,eAAuB;eAJ1B,EAAE,MAAM,QAAQ;AAKxD,OAAK,UAAU;AACf,OAAK,gBAAgB;;CAIvB,eAAe,cAA4D;AACzE,MAAI,KAAK,MAAM,SAAS,eACtB,QAAO;EAIT,MAAM,qBADa,KAAK,gCAAgC,IACf,KAAK;AAI9C,MAAI,KAAK,QAAQ,eAAe,QAC9B,QAAO,sBAAsB,iBAAiB;MAE9C,QAAO;;CAKX,gBACE,cAC0B;AAC1B,MAAI,KAAK,MAAM,SAAS,eACtB,QAAO;EAGT,MAAM,aAAa,iBACjB,KAAK,MAAM,UACX,KAAK,QAAQ,UACb,KAAK,QAAQ,UACd;EACD,MAAM,aAAa,iBACjB,KAAK,MAAM,UACX,KAAK,QAAQ,UACb,KAAK,QAAQ,UACd;EACD,MAAM,kBAAkB,KAAK,yBAC3B,YACA,aACD;EACD,MAAM,EAAE,QAAQ,SAAS,0BAA0B,KAAK,MAAM,UAAU;AAExE,SAAO;GACL,KAAK;GACL,KAAK;GACL,UAAU;GACV;GACA;GACD;;CAIH,YAAY,UAAoB,cAAwC;AACtE,MAAI,KAAK,MAAM,SAAS,OAItB,MAAK,QAAQ;GACX,MAAM;GACN,UAJe,KAAK,YAAY,aAAa;GAK7C,UAJe,KAAK,YAAY,aAAa;GAK7C,WAAW,CAAC;IAAE;IAAU;IAAc,CAAC;GACxC;MAGD,MAAK,MAAM,UAAU,KAAK;GAAE;GAAU;GAAc,CAAC;;CAKzD,QAAc;AACZ,OAAK,QAAQ,EAAE,MAAM,QAAQ;;CAI/B,gBAAgB,UAA8C;AAC5D,OAAK,QAAQ;GACX,MAAM;GACN,UAAU,SAAS;GACnB,UAAU,SAAS;GACnB,WAAW,EAAE;GACd;;CAIH,WAAqC;AACnC,SAAO,KAAK;;CAId,iBAA0B;AACxB,SAAO,KAAK,MAAM,SAAS;;CAI7B,AAAQ,iCAAyC;AAC/C,MAAI,KAAK,MAAM,SAAS,eACtB,QAAO;EAGT,MAAM,WAAW,KAAK,MAAM,UAAU,KAAK,MAAM,UAAU,SAAS;EACpE,MAAM,aAAa,KAAK,cAAc,SAAS,aAAa;AAW5D,SAAO,wBAVQ,iBACb,WAAW,OAAO,WAAW,YAAY,IACzC,KAAK,QAAQ,UACb,KAAK,QAAQ,UACd,GACgB,iBACf,KAAK,MAAM,UACX,KAAK,QAAQ,UACb,KAAK,QAAQ,UACd,EACiD,KAAK,QAAQ,UAAU;;CAG3E,AAAQ,yBACN,YACA,cACQ;AACR,MAAI,aAMF,QALuB,iBACrB,aAAa,KACb,KAAK,QAAQ,UACb,KAAK,QAAQ,UACd,GACuB;EAK1B,MAAM,gBAAgB,CAAC,GAAG,KAAK,QAAQ,cAAc,CAAC,MACnD,GAAG,MAAM,EAAE,MAAM,EAAE,IACrB;EACD,MAAM,aAAa,cAAc,cAAc,SAAS;AAMxD,SALkB,iBAChB,WAAW,OAAO,WAAW,YAAY,IACzC,KAAK,QAAQ,UACb,KAAK,QAAQ,UACd,GACkB;;CAGrB,AAAQ,YAAY,cAA0C;AAC5D,MAAI,KAAK,QAAQ,eAAe,QAE9B,QADiB,aAAa,aAAa,MAAM,MAAM,EAAE,WAAW,EACnD,OAAO,aAAa,aAAa,IAAI,OAAO;MAE7D,QAAO,aAAa,aAAa,IAAI,OAAO;;CAIhD,AAAQ,YAAY,cAA0C;AAC5D,MAAI,KAAK,QAAQ,eAAe,QAE9B,QADiB,aAAa,aAAa,MAAM,MAAM,EAAE,WAAW,EACnD,OAAO,aAAa,aAAa,IAAI,OAAO;MAE7D,QAAO,aAAa,aAAa,IAAI,OAAO;;CAIhD,AAAQ,cAAc,cAGpB;AACA,MAAI,KAAK,QAAQ,eAAe,SAAS;GACvC,MAAM,UAAU,aAAa;AAC7B,UAAO,QAAQ,QAAQ,SAAS;SAC3B;GACL,MAAM,UAAU,aAAa;AAC7B,UAAO,QAAQ,QAAQ,SAAS;;;;AAOtC,MAAa,wBAAwB,OACnC,aACA,mBACA,gBACA,YACgD;CAEhD,MAAM,SAAS,IAAI,oBAAoB;CAIvC,MAAM,+BADU,SAAS,+BAAkB,EAGzC,yCAAwB,EAAE,CAAC,SAAS,MAAM,CAAC,MAC5C;CACD,IAAI,YAAY;CAEhB,MAAM,OAAO,IAAIC,qBAAS,EACxB,MAAM,OAAO,WAAW,UAAU;AAChC,eAAa,MAAM;AACnB,YAAU;IAEb,CAAC;CAEF,MAAM,iDAAoC,SAAS;AAgBnD,0CAAe,aAZM,IAAIF,sBAAU;EACjC,UAAU,OAAO,WAAW,UAAU;AACpC,mBAAgB,MAAM,MAAM;AAC5B,QAAK,KAAK,MAAM;AAChB,aAAU;;EAEZ,MAAM,UAAU;AACd,mBAAgB,UAAU,UAAU,CAAC;;EAExC,CAAC,EAGwC,QAAQ,KAAK;CACvD,MAAM,YAAY,OAAO,cAAc;AAGvC,KAAI,cAAc,GAAG;AACnB,qCAAa,SAAS,CAAC,YAAY,GAAG;AACtC,SAAO,EAAE;;CAIX,IAAIG;AACJ,KAAI;AACF,UAAQ,MAAMC,0BAAY,UAAU,SAAS;UACtC,OAAO;AACd,UAAQ,KAAK,wCAAwC,MAAM;AAC3D,qCAAa,SAAS,CAAC,YAAY,GAAG;AACtC,SAAO,EAAE;WACD;AACR,qCAAa,SAAS,CAAC,YAAY,GAAG;;CAGxC,MAAM,eAAe,MAAM;CAC3B,MAAM,eAAe,MAAM;CAE3B,MAAMC,eAAmD,EAAE;CAC3D,MAAM,eAAe,UAAU,MAAM,MAAM,EAAE,SAAS,OAAO;CAC7D,MAAM,iBAAiB,UAAU,QAAQ,MAAM,EAAE,SAAS,QAAQ;CAIlE,MAAMC,qBAA2C,EAAE;AAEnD,MACE,IAAI,gBAAgB,GACpB,gBAAgB,eAAe,QAC/B,iBACA;EACA,MAAM,WAAW,eAAe;EAGhC,MAAM,gBAAgB,SAAS;EAC/B,MAAM,cAAc,SAAS,SAAS,SAAS;EAE/C,MAAM,eAAe,MAAM,QACxB,QAAQ,WAAW;AAIlB,UAHe,aAAa,MACzB,MAAM,EAAE,UAAU,OAAO,aAC3B,EAES,eAAe,WACvB,OAAO,QAAQ,UACf,OAAO,OAAO,iBACd,OAAO,MAAM;IAEf,CACD,KAAK,YAAY;GAChB,KAAK,OAAO;GACZ,KAAK,OAAO;GACZ,UAAU,OAAO;GACjB,YAAY,OAAO,OAAO,SAAS,IAAI,IAAI;GAC5C,EAAE;EAEL,MAAM,eAAe,MAAM,QACxB,QAAQ,WAAW;AAIlB,UAHe,aAAa,MACzB,MAAM,EAAE,UAAU,OAAO,aAC3B,EAES,eAAe,WACvB,OAAO,QAAQ,UACf,OAAO,OAAO,iBACd,OAAO,MAAM;IAEf,CACD,KAAK,YAAY;GAChB,KAAK,OAAO;GACZ,KAAK,OAAO;GACZ,UAAU,OAAO;GAClB,EAAE;AAEL,qBAAmB,KAAK;GACtB;GACA;GACA;GACD,CAAC;;CAIJ,MAAM,gBACJ,aACA,YACA,UACA,eACmB;EACnB,MAAMC,WAA2B,EAAE;EAkBnC,MAAM,cAAc,IAAI,mBAVgB;GACtC;GACA,WATgB,KAAK,MAAM,SAAS,MAAM,SAAS,IAAI;GAUvD;GACA;GACA,eAToB,WAAW,QAC9B,MAAM,EAAE,iBAAiB,YAC3B;GAQC;GACA;GACD,EAIC,wBACD;AAED,OAAK,IAAI,IAAI,GAAG,IAAI,mBAAmB,QAAQ,KAAK;GAClD,MAAM,eAAe,mBAAmB;GACxC,MAAM,WAAW,eAAe,aAAa;GAC7C,MAAM,UACJ,eAAe,UACX,aAAa,eACb,aAAa;AAEnB,OACE,YAAY,aAAa,cAAc,IAAI,QAAQ,OAAO,GAAG,WAAW,UACzE;AAED,OAAI,QAAQ,WAAW,GAAG;AACxB,QACE,qBAAqB,aAAa,cAAc,QAAQ,WAAW,UACpE;AACD;;AAGF,OAAI,eAAe,SAAS;IAE1B,MAAM,WAAW,aAAa,aAAa,MAAM,MAAM,EAAE,WAAW;IACpE,MAAM,cAAc,aAAa;AAGjC,QAAI,CAAC,YAAY,gBAAgB,IAAI,aAAa;AAChD,iBAAY,gBAAgB;MAC1B,KAAK,SAAS;MACd,KAAK,SAAS;MACf,CAAC;AACF,iBAAY,YAAY,UAAU,aAAa;AAC/C;;AAIF,QAAI,CAAC,YAAY,gBAAgB,CAC/B;AAIF,QAAI,aACF;SACE,YAAY,eAAe;MAAE,KAAK,SAAS;MAAK,KAAK,SAAS;MAAK,CAAC,EACpE;MAEA,MAAM,eAAe,EAAE,KAAK,SAAS,KAAK;MAC1C,MAAM,aAAa,YAAY,gBAAgB,aAAa;AAC5D,UAAI,WACF,UAAS,KAAK,WAAW;AAE3B,kBAAY,OAAO;AACnB,kBAAY,gBAAgB;OAC1B,KAAK,SAAS;OACd,KAAK,SAAS;OACf,CAAC;;;UAGD;AAEL,QAAI,CAAC,YAAY,gBAAgB,EAAE;AACjC,iBAAY,YAAY,UAAU,aAAa;AAC/C;;AAIF,QAAI,YAAY,eAAe,KAAK,EAAE;KAEpC,MAAM,eAAe,EAAE,KAAK,aAAa,aAAa,GAAI,KAAK;KAC/D,MAAM,aAAa,YAAY,gBAAgB,aAAa;AAC5D,SAAI,WACF,UAAS,KAAK,WAAW;AAE3B,iBAAY,OAAO;;;AAKvB,eAAY,YAAY,UAAU,aAAa;;AAIjD,MAAI,YAAY,gBAAgB,EAAE;GAChC,MAAM,aAAa,YAAY,gBAAgB,KAAK;AACpD,OAAI,WACF,UAAS,KAAK,WAAW;;AAI7B,SAAO;;AAIT,MAAK,MAAM,eAAe,cAAc;EAEtC,MAAM,WAAW,MAAM;AACvB,MAAI,CAAC,UAAU;AACb,WAAQ,KAAK,qCAAqC;AAClD;;EAGF,MAAM,YAAY,KAAK,MAAM,SAAS,MAAM,SAAS,IAAI;EAGzD,MAAM,gBAAiB,MAAM,QAA0B,QACpD,MAAM,EAAE,iBAAiB,YAAY,MACvC;EACD,MAAM,gBAAgB,cAAc,QAAQ,MAC1C,EAAE,OAAO,SAAS,IAAI,CACvB,CAAC;EACF,MAAM,mBAAmB,cAAc;AAEvC,MACE,uBAAuB,cAAc,OAAO,kBAAkB,cAAc,wBAAwB,YAAY,QACjH;EAGD,IAAIC;AACJ,MAAI,cAAc,SAAS,GAAG;AAC5B,OACE,gCAAgC,cAAc,GAAI,SAAS,cAAc,cAAc,GAAI,WAC5F;GACD,MAAM,mBAAmB,cAAc,GAAI;AAC3C,OAAI,KAAK,IAAI,iBAAiB,GAAG,IAC/B,0BAAyB,mBAAmB;;AAGhD,MAAI,sBAAsB,OACxB,0BAAyB;EAI3B,MAAM,WAAW,aACf,YAAY,OACZ,SACA,UACA,MAAM,QACP;EAGD,IAAI,gBAAgB;AACpB,MAAI,cAAc,SAAS,GAAG;GAC5B,MAAM,cAAc,cAAc;GAClC,MAAM,aAAa,cAAc,cAAc,SAAS;GACxD,MAAM,WAAW,iBAAiB,YAAY,KAAK,UAAU,UAAU;GACvE,MAAM,UAAU,iBAAiB,WAAW,KAAK,UAAU,UAAU;GACrE,MAAM,eAAe,iBACnB,WAAW,YAAY,GACvB,UACA,UACD;AACD,mBAAgB,UAAU,WAAW;;EAGvC,MAAM,eACJ,iBAAiB,YAAY,UAAU,YAAY,QAAQ;AAC7D,eAAa,gBAAgB;GAC3B,OAAO;GACP,MAAM;GACN,OAAO,YAAY,eAAe,YAAY;GAC9C,QAAQ,YAAY,gBAAgB,YAAY;GACrC;GACX,cAAc;GACd,OAAO,yBACL,YAAY,kBACZ,YAAY,SACZ,YAAY,MACb;GACD,UAAU;GACV,mBAAmB;GACnB,aAAa;IACX,QAAQ;IACR,MAAM,cAAc,QAAQ;IAC7B;GACD;GACD;;AAIH,MAAK,MAAM,eAAe,cAAc;EAEtC,MAAM,WAAW,MAAM;AACvB,MAAI,CAAC,UAAU;AACb,WAAQ,KAAK,qCAAqC;AAClD;;EAGF,MAAM,YAAY,KAAK,MAAM,SAAS,MAAM,SAAS,IAAI;EAGzD,MAAM,gBAAiB,MAAM,QAA0B,QACpD,MAAM,EAAE,iBAAiB,YAAY,MACvC;EACD,MAAM,mBAAmB,cAAc;EAGvC,IAAIA;AACJ,MAAI,cAAc,SAAS,GAAG;GAC5B,MAAM,mBAAmB,cAAc,GAAI;AAC3C,OAAI,KAAK,IAAI,iBAAiB,GAAG,IAC/B,0BAAyB,mBAAmB;;AAGhD,MAAI,sBAAsB,OACxB,0BAAyB;EAI3B,MAAM,WAAW,aACf,YAAY,OACZ,SACA,UACA,MAAM,QACP;EAGD,MAAM,gBAAgB,SAAS,QAAQ,KAAK,QAAQ,MAAM,IAAI,UAAU,EAAE;EAE1E,MAAM,eACJ,iBAAiB,YAAY,UAAU,YAAY,QAAQ;AAC7D,eAAa,gBAAgB;GAC3B,OAAO;GACP,MAAM;GACN,eAAe,YAAY;GAC3B,aAAa,OAAO,YAAY,YAAY;GAC5C,aAAa,YAAY;GACzB,cAAc;GACH;GACX,OAAO,YAAY,oBAAoB,YAAY,cAAc;GACjE,UAAU;GACV,mBAAmB;GACnB,aAAa;IACX,QAAQ;IACR,MAAM,cAAc,QAAQ;IAC7B;GACD;GACD;;AAGH,QAAO"}
@@ -2,7 +2,11 @@ import { TrackFragmentIndex } from "./Probe.cjs";
2
2
  import { Readable } from "node:stream";
3
3
 
4
4
  //#region src/generateFragmentIndex.d.ts
5
- declare const generateFragmentIndex: (inputStream: Readable, startTimeOffsetMs?: number, trackIdMapping?: Record<number, number>) => Promise<Record<number, TrackFragmentIndex>>;
5
+ declare const generateFragmentIndex: (inputStream: Readable, startTimeOffsetMs?: number, trackIdMapping?: Record<number, number>,
6
+ // Map from source track ID to desired track ID
7
+ options?: {
8
+ tmpDir?: string;
9
+ }) => Promise<Record<number, TrackFragmentIndex>>;
6
10
  //#endregion
7
11
  export { generateFragmentIndex };
8
12
  //# sourceMappingURL=generateFragmentIndex.d.cts.map
@@ -2,7 +2,11 @@ import { TrackFragmentIndex } from "./Probe.js";
2
2
  import { Readable } from "node:stream";
3
3
 
4
4
  //#region src/generateFragmentIndex.d.ts
5
- declare const generateFragmentIndex: (inputStream: Readable, startTimeOffsetMs?: number, trackIdMapping?: Record<number, number>) => Promise<Record<number, TrackFragmentIndex>>;
5
+ declare const generateFragmentIndex: (inputStream: Readable, startTimeOffsetMs?: number, trackIdMapping?: Record<number, number>,
6
+ // Map from source track ID to desired track ID
7
+ options?: {
8
+ tmpDir?: string;
9
+ }) => Promise<Record<number, TrackFragmentIndex>>;
6
10
  //#endregion
7
11
  export { generateFragmentIndex };
8
12
  //# sourceMappingURL=generateFragmentIndex.d.ts.map
@@ -1,7 +1,12 @@
1
1
  import { PacketProbe } from "./Probe.js";
2
+ import { createWriteStream } from "node:fs";
2
3
  import debug from "debug";
3
- import { Readable, Transform, Writable } from "node:stream";
4
+ import { Transform, Writable } from "node:stream";
4
5
  import { pipeline } from "node:stream/promises";
6
+ import { unlink } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { randomBytes } from "node:crypto";
5
10
 
6
11
  //#region src/generateFragmentIndex.ts
7
12
  const log = debug("ef:generateFragmentIndex");
@@ -95,19 +100,6 @@ var StreamingBoxParser = class extends Transform {
95
100
  return this.fragments;
96
101
  }
97
102
  };
98
- function createFragmentStream(fragmentData) {
99
- let offset = 0;
100
- return new Readable({ read() {
101
- if (offset >= fragmentData.length) {
102
- this.push(null);
103
- return;
104
- }
105
- const chunkSize = Math.min(64 * 1024, fragmentData.length - offset);
106
- const chunk = fragmentData.slice(offset, offset + chunkSize);
107
- offset += chunkSize;
108
- this.push(Buffer.from(chunk));
109
- } });
110
- }
111
103
  function convertTimestamp(pts, timebase, timescale) {
112
104
  return Math.round(pts * timescale / timebase.den);
113
105
  }
@@ -210,25 +202,39 @@ var SegmentAccumulator = class {
210
202
  }
211
203
  }
212
204
  };
213
- const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapping) => {
205
+ const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapping, options) => {
214
206
  const parser = new StreamingBoxParser();
215
- const chunks = [];
207
+ const tempFile = join(options?.tmpDir ?? tmpdir(), `ef-probe-${randomBytes(8).toString("hex")}.mp4`);
216
208
  let totalSize = 0;
217
- await pipeline(inputStream, parser, new Writable({ write(chunk, _encoding, callback) {
218
- chunks.push(chunk);
209
+ const dest = new Writable({ write(chunk, _encoding, callback) {
219
210
  totalSize += chunk.length;
220
211
  callback();
221
- } }));
212
+ } });
213
+ const tempWriteStream = createWriteStream(tempFile);
214
+ await pipeline(inputStream, new Transform({
215
+ transform(chunk, _encoding, callback) {
216
+ tempWriteStream.write(chunk);
217
+ this.push(chunk);
218
+ callback();
219
+ },
220
+ flush(callback) {
221
+ tempWriteStream.end(() => callback());
222
+ }
223
+ }), parser, dest);
222
224
  const fragments = parser.getFragments();
223
- if (totalSize === 0) return {};
224
- const completeData = Buffer.concat(chunks);
225
- const completeStream = createFragmentStream(new Uint8Array(completeData.buffer, completeData.byteOffset, completeData.byteLength));
225
+ if (totalSize === 0) {
226
+ await unlink(tempFile).catch(() => {});
227
+ return {};
228
+ }
226
229
  let probe;
227
230
  try {
228
- probe = await PacketProbe.probeStream(completeStream);
231
+ probe = await PacketProbe.probePath(tempFile);
229
232
  } catch (error) {
230
233
  console.warn("Failed to probe stream with ffprobe:", error);
234
+ await unlink(tempFile).catch(() => {});
231
235
  return {};
236
+ } finally {
237
+ await unlink(tempFile).catch(() => {});
232
238
  }
233
239
  const videoStreams = probe.videoStreams;
234
240
  const audioStreams = probe.audioStreams;
@@ -336,9 +342,9 @@ const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapp
336
342
  }
337
343
  const timescale = Math.round(timebase.den / timebase.num);
338
344
  const streamPackets = probe.packets.filter((p) => p.stream_index === videoStream.index);
339
- const keyframePackets = streamPackets.filter((p) => p.flags?.includes("K"));
340
- const totalSampleCount = keyframePackets.length;
341
- log(`Complete stream has ${streamPackets.length} video packets, ${keyframePackets.length} keyframes for stream ${videoStream.index}`);
345
+ const keyframeCount = streamPackets.filter((p) => p.flags?.includes("K")).length;
346
+ const totalSampleCount = streamPackets.length;
347
+ log(`Complete stream has ${streamPackets.length} video packets, ${keyframeCount} keyframes for stream ${videoStream.index}`);
342
348
  let trackStartTimeOffsetMs;
343
349
  if (streamPackets.length > 0) {
344
350
  log(`First video packet dts_time: ${streamPackets[0].dts_time}, pts_time: ${streamPackets[0].pts_time}`);
@@ -352,7 +358,9 @@ const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapp
352
358
  const firstPacket = streamPackets[0];
353
359
  const lastPacket = streamPackets[streamPackets.length - 1];
354
360
  const firstPts = convertTimestamp(firstPacket.pts, timebase, timescale);
355
- totalDuration = convertTimestamp(lastPacket.pts, timebase, timescale) - firstPts;
361
+ const lastPts = convertTimestamp(lastPacket.pts, timebase, timescale);
362
+ const lastDuration = convertTimestamp(lastPacket.duration ?? 0, timebase, timescale);
363
+ totalDuration = lastPts - firstPts + lastDuration;
356
364
  }
357
365
  const finalTrackId = trackIdMapping?.[videoStream.index] ?? videoStream.index + 1;
358
366
  trackIndexes[finalTrackId] = {