@editframe/assets 0.18.7-beta.0 → 0.18.19-beta.0

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,3 @@
1
+ import { Readable } from 'node:stream';
2
+ import { TrackFragmentIndex } from './Probe.js';
3
+ export declare const generateFragmentIndex: (inputStream: Readable, startTimeOffsetMs?: number, trackIdMapping?: Record<number, number>) => Promise<Record<number, TrackFragmentIndex>>;
@@ -2,7 +2,7 @@ import debug from "debug";
2
2
  import { Transform, Writable } from "node:stream";
3
3
  import { pipeline } from "node:stream/promises";
4
4
  import { EncodedPacketSink, Input, MP4, StreamSource } from "mediabunny";
5
- const log = debug("ef:generateTrackFragmentIndexMediabunny");
5
+ const log = debug("ef:generateFragmentIndex");
6
6
  /**
7
7
  * Streaming MP4 box parser that detects box boundaries without loading entire file into memory
8
8
  */
@@ -83,12 +83,12 @@ var StreamingBoxParser = class extends Transform {
83
83
  return this.fragments;
84
84
  }
85
85
  };
86
- function extractFragmentData(mediabunnyChunks, initFragment, mediaFragment) {
86
+ function extractFragmentData(chunks, initFragment, mediaFragment) {
87
87
  const extractBytes = (offset, size) => {
88
88
  const buffer = Buffer.alloc(size);
89
89
  let written = 0;
90
90
  let currentOffset = 0;
91
- for (const chunk of mediabunnyChunks) {
91
+ for (const chunk of chunks) {
92
92
  if (currentOffset + chunk.length <= offset) {
93
93
  currentOffset += chunk.length;
94
94
  continue;
@@ -112,16 +112,16 @@ function extractFragmentData(mediabunnyChunks, initFragment, mediaFragment) {
112
112
  combined.set(mediaData, initData.length);
113
113
  return combined;
114
114
  }
115
- const generateTrackFragmentIndexMediabunny = async (inputStream, startTimeOffsetMs, trackIdMapping) => {
115
+ const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapping) => {
116
116
  const parser = new StreamingBoxParser();
117
- const mediabunnyChunks = [];
117
+ const chunks = [];
118
118
  let totalSize = 0;
119
- const mediabunnyDest = new Writable({ write(chunk, _encoding, callback) {
120
- mediabunnyChunks.push(chunk);
119
+ const dest = new Writable({ write(chunk, _encoding, callback) {
120
+ chunks.push(chunk);
121
121
  totalSize += chunk.length;
122
122
  callback();
123
123
  } });
124
- await pipeline(inputStream, parser, mediabunnyDest);
124
+ await pipeline(inputStream, parser, dest);
125
125
  const fragments = parser.getFragments();
126
126
  if (totalSize === 0) return {};
127
127
  const source = new StreamSource({
@@ -130,7 +130,7 @@ const generateTrackFragmentIndexMediabunny = async (inputStream, startTimeOffset
130
130
  const buffer = Buffer.alloc(size);
131
131
  let written = 0;
132
132
  let currentOffset = 0;
133
- for (const chunk of mediabunnyChunks) {
133
+ for (const chunk of chunks) {
134
134
  if (currentOffset + chunk.length <= start) {
135
135
  currentOffset += chunk.length;
136
136
  continue;
@@ -159,7 +159,7 @@ const generateTrackFragmentIndexMediabunny = async (inputStream, startTimeOffset
159
159
  videoTracks = await input.getVideoTracks();
160
160
  audioTracks = await input.getAudioTracks();
161
161
  } catch (error) {
162
- console.warn("Failed to parse with Mediabunny:", error);
162
+ console.warn("Failed to parse with parser:", error);
163
163
  return {};
164
164
  }
165
165
  const trackIndexes = {};
@@ -169,7 +169,7 @@ const generateTrackFragmentIndexMediabunny = async (inputStream, startTimeOffset
169
169
  const audioFragmentTimings = [];
170
170
  for (let fragmentIndex = 0; fragmentIndex < mediaFragments.length; fragmentIndex++) {
171
171
  const fragment = mediaFragments[fragmentIndex];
172
- const fragmentData = extractFragmentData(mediabunnyChunks, initFragment, fragment);
172
+ const fragmentData = extractFragmentData(chunks, initFragment, fragment);
173
173
  const fragmentSource = new StreamSource({
174
174
  read: async (start, end) => fragmentData.subarray(start, end),
175
175
  getSize: async () => fragmentData.length
@@ -250,15 +250,19 @@ const generateTrackFragmentIndexMediabunny = async (inputStream, startTimeOffset
250
250
  }
251
251
  if (startTimeOffsetMs !== void 0) trackStartTimeOffsetMs = startTimeOffsetMs;
252
252
  const timescale = Math.round(track.timeResolution);
253
+ let accumulatedDts = 0;
254
+ let accumulatedCts = 0;
253
255
  for (const timing of videoFragmentTimings) {
254
256
  const fragment = mediaFragments[timing.fragmentIndex];
255
257
  segments.push({
256
- cts: timing.cts,
257
- dts: timing.dts,
258
+ cts: accumulatedCts,
259
+ dts: accumulatedDts,
258
260
  duration: timing.duration,
259
261
  offset: fragment.offset,
260
262
  size: fragment.size
261
263
  });
264
+ accumulatedDts += timing.duration;
265
+ accumulatedCts += timing.duration;
262
266
  totalDuration += timing.duration / timescale;
263
267
  }
264
268
  let width = 1920;
@@ -308,15 +312,19 @@ const generateTrackFragmentIndexMediabunny = async (inputStream, startTimeOffset
308
312
  }
309
313
  if (startTimeOffsetMs !== void 0) trackStartTimeOffsetMs = startTimeOffsetMs;
310
314
  const timescale = Math.round(track.timeResolution);
315
+ let accumulatedDts = 0;
316
+ let accumulatedCts = 0;
311
317
  for (const timing of audioFragmentTimings) {
312
318
  const fragment = mediaFragments[timing.fragmentIndex];
313
319
  segments.push({
314
- cts: timing.cts,
315
- dts: timing.dts,
320
+ cts: accumulatedCts,
321
+ dts: accumulatedDts,
316
322
  duration: timing.duration,
317
323
  offset: fragment.offset,
318
324
  size: fragment.size
319
325
  });
326
+ accumulatedDts += timing.duration;
327
+ accumulatedCts += timing.duration;
320
328
  totalDuration += timing.duration / timescale;
321
329
  }
322
330
  const finalTrackId = trackIdMapping?.[track.id] ?? track.id;
@@ -340,4 +348,4 @@ const generateTrackFragmentIndexMediabunny = async (inputStream, startTimeOffset
340
348
  }
341
349
  return trackIndexes;
342
350
  };
343
- export { generateTrackFragmentIndexMediabunny };
351
+ export { generateFragmentIndex };
@@ -0,0 +1,8 @@
1
+ import { PassThrough } from 'node:stream';
2
+ export declare const generateSingleTrackFromPath: (absolutePath: string, trackId: number) => Promise<{
3
+ stream: PassThrough;
4
+ fragmentIndex: Promise<Record<number, import('./Probe.js').TrackFragmentIndex>>;
5
+ }>;
6
+ export declare const generateSingleTrackTask: (rootDir: string, absolutePath: string, trackId: number) => Promise<import('./idempotentTask.js').TaskResult>;
7
+ export declare const generateSingleTrack: (cacheRoot: string, absolutePath: string, url: string) => Promise<import('./idempotentTask.js').TaskResult>;
8
+ export declare const generateSingleTrackWithIndex: (absolutePath: string, trackId: number) => Promise<PassThrough>;
@@ -1,11 +1,11 @@
1
1
  import { Probe } from "./Probe.js";
2
+ import { generateFragmentIndex } from "./generateFragmentIndex.js";
2
3
  import { idempotentTask } from "./idempotentTask.js";
3
- import { generateTrackFragmentIndexMediabunny } from "./generateTrackFragmentIndexMediabunny.js";
4
4
  import debug from "debug";
5
- import { basename } from "node:path";
6
5
  import { PassThrough } from "node:stream";
7
- const log = debug("ef:generateTrackMediabunny");
8
- const generateTrackFromPathMediabunny = async (absolutePath, trackId) => {
6
+ import { basename } from "node:path";
7
+ const log = debug("ef:generateSingleTrack");
8
+ const generateSingleTrackFromPath = async (absolutePath, trackId) => {
9
9
  log(`Generating track ${trackId} for ${absolutePath}`);
10
10
  const probe = await Probe.probePath(absolutePath);
11
11
  const streamIndex = trackId - 1;
@@ -24,7 +24,7 @@ const generateTrackFromPathMediabunny = async (absolutePath, trackId) => {
24
24
  indexStream.destroy(error);
25
25
  });
26
26
  const trackIdMapping = { 1: trackId };
27
- const fragmentIndexPromise = generateTrackFragmentIndexMediabunny(indexStream, void 0, trackIdMapping);
27
+ const fragmentIndexPromise = generateFragmentIndex(indexStream, void 0, trackIdMapping);
28
28
  fragmentIndexPromise.then(() => {
29
29
  if (sourceStreamEnded) outputStream.end();
30
30
  else trackStream.once("end", () => {
@@ -38,32 +38,37 @@ const generateTrackFromPathMediabunny = async (absolutePath, trackId) => {
38
38
  fragmentIndex: fragmentIndexPromise
39
39
  };
40
40
  };
41
- const generateTrackTaskMediabunny = idempotentTask({
42
- label: "track-mediabunny",
41
+ const generateSingleTrackTask = idempotentTask({
42
+ label: "track-single",
43
43
  filename: (absolutePath, trackId) => `${basename(absolutePath)}.track-${trackId}.mp4`,
44
44
  runner: async (absolutePath, trackId) => {
45
- const result = await generateTrackFromPathMediabunny(absolutePath, trackId);
45
+ const result = await generateSingleTrackFromPath(absolutePath, trackId);
46
46
  const finalStream = new PassThrough();
47
- let streamEnded = false;
48
- let fragmentIndexCompleted = false;
49
- const checkCompletion = () => {
50
- if (streamEnded && fragmentIndexCompleted) finalStream.end();
51
- };
52
- result.stream.pipe(finalStream, { end: false });
53
- result.stream.on("end", () => {
54
- streamEnded = true;
55
- checkCompletion();
47
+ const fragmentIndexPromise = result.fragmentIndex.catch((error) => {
48
+ console.warn(`Fragment index generation failed for track ${trackId}:`, error);
56
49
  });
57
- result.stream.on("error", (error) => {
58
- finalStream.destroy(error);
50
+ let progressTimeout = null;
51
+ const resetProgressTimeout = () => {
52
+ if (progressTimeout) clearTimeout(progressTimeout);
53
+ progressTimeout = setTimeout(() => {
54
+ if (!finalStream.destroyed) {
55
+ console.warn(`Progress timeout triggered for track ${trackId} - no activity for 10 seconds`);
56
+ finalStream.end();
57
+ }
58
+ }, 1e4);
59
+ };
60
+ resetProgressTimeout();
61
+ result.stream.on("data", () => {
62
+ resetProgressTimeout();
59
63
  });
60
- result.fragmentIndex.then(() => {
61
- fragmentIndexCompleted = true;
62
- checkCompletion();
63
- }).catch((error) => {
64
- finalStream.destroy(error);
64
+ result.stream.on("end", () => {
65
+ resetProgressTimeout();
65
66
  });
67
+ result.stream.pipe(finalStream, { end: false });
68
+ await fragmentIndexPromise;
69
+ finalStream.end();
70
+ if (progressTimeout) clearTimeout(progressTimeout);
66
71
  return finalStream;
67
72
  }
68
73
  });
69
- export { generateTrackFromPathMediabunny };
74
+ export { generateSingleTrackFromPath };
@@ -1,9 +1,9 @@
1
1
  import { md5FilePath } from "./md5.js";
2
2
  import { createWriteStream, existsSync } from "node:fs";
3
3
  import debug from "debug";
4
+ import { Readable } from "node:stream";
4
5
  import { mkdir, stat, writeFile } from "node:fs/promises";
5
6
  import path from "node:path";
6
- import { Readable } from "node:stream";
7
7
  const idempotentTask = ({ label, filename, runner }) => {
8
8
  const tasks = {};
9
9
  const downloadTasks = {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { StreamSchema, AudioStreamSchema, VideoStreamSchema, ProbeSchema, PacketProbeSchema, TrackSegment, TrackFragmentIndex, AudioTrackFragmentIndex, VideoTrackFragmentIndex, } from './Probe.js';
2
2
  export { Probe, PacketProbe } from './Probe.js';
3
+ export { generateFragmentIndex } from './generateFragmentIndex.js';
3
4
  export { md5FilePath, md5Directory, md5ReadStream, md5Buffer } from './md5.js';
4
5
  export { generateTrackFragmentIndex, generateTrackFragmentIndexFromPath, } from './tasks/generateTrackFragmentIndex.js';
5
6
  export { generateTrack, generateTrackFromPath } from './tasks/generateTrack.js';
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { PacketProbe, Probe } from "./Probe.js";
2
+ import { generateFragmentIndex } from "./generateFragmentIndex.js";
2
3
  import { md5Buffer, md5Directory, md5FilePath, md5ReadStream } from "./md5.js";
3
4
  import { generateTrackFragmentIndex, generateTrackFragmentIndexFromPath } from "./tasks/generateTrackFragmentIndex.js";
4
5
  import { generateTrack, generateTrackFromPath } from "./tasks/generateTrack.js";
5
6
  import { findOrCreateCaptions, generateCaptionDataFromPath } from "./tasks/findOrCreateCaptions.js";
6
7
  import { cacheImage } from "./tasks/cacheImage.js";
7
8
  import { VideoRenderOptions } from "./VideoRenderOptions.js";
8
- export { PacketProbe, Probe, VideoRenderOptions, cacheImage, findOrCreateCaptions, generateCaptionDataFromPath, generateTrack, generateTrackFragmentIndex, generateTrackFragmentIndexFromPath, generateTrackFromPath, md5Buffer, md5Directory, md5FilePath, md5ReadStream };
9
+ export { PacketProbe, Probe, VideoRenderOptions, cacheImage, findOrCreateCaptions, generateCaptionDataFromPath, generateFragmentIndex, generateTrack, generateTrackFragmentIndex, generateTrackFragmentIndexFromPath, generateTrackFromPath, md5Buffer, md5Directory, md5FilePath, md5ReadStream };
@@ -1,11 +1,11 @@
1
1
  import { idempotentTask } from "../idempotentTask.js";
2
- import { generateTrackFromPathMediabunny } from "../generateTrackMediabunny.js";
2
+ import { generateSingleTrackFromPath } from "../generateSingleTrack.js";
3
3
  import debug from "debug";
4
4
  import { basename } from "node:path";
5
5
  const generateTrackFromPath = async (absolutePath, trackId) => {
6
6
  const log = debug("ef:generateTrackFragment");
7
- log(`Generating track ${trackId} for ${absolutePath} using Mediabunny`);
8
- const result = await generateTrackFromPathMediabunny(absolutePath, trackId);
7
+ log(`Generating track ${trackId} for ${absolutePath}`);
8
+ const result = await generateSingleTrackFromPath(absolutePath, trackId);
9
9
  return result.stream;
10
10
  };
11
11
  const generateTrackTask = idempotentTask({
@@ -1,6 +1,6 @@
1
1
  import { Probe } from "../Probe.js";
2
+ import { generateFragmentIndex } from "../generateFragmentIndex.js";
2
3
  import { idempotentTask } from "../idempotentTask.js";
3
- import { generateTrackFragmentIndexMediabunny } from "../generateTrackFragmentIndexMediabunny.js";
4
4
  import debug from "debug";
5
5
  import { basename } from "node:path";
6
6
  const generateTrackFragmentIndexFromPath = async (absolutePath) => {
@@ -26,7 +26,7 @@ const generateTrackFragmentIndexFromPath = async (absolutePath) => {
26
26
  log(`Processing track ${trackId} (${stream.codec_type})`);
27
27
  const trackStream = probe.createTrackReadstream(streamIndex);
28
28
  const trackIdMapping = { 1: trackId };
29
- const singleTrackIndexes = await generateTrackFragmentIndexMediabunny(trackStream, startTimeOffsetMs, trackIdMapping);
29
+ const singleTrackIndexes = await generateFragmentIndex(trackStream, startTimeOffsetMs, trackIdMapping);
30
30
  Object.assign(trackFragmentIndexes, singleTrackIndexes);
31
31
  }
32
32
  return trackFragmentIndexes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/assets",
3
- "version": "0.18.7-beta.0",
3
+ "version": "0.18.19-beta.0",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -3,8 +3,8 @@ import { generateTrackFromPath } from "./generateTrack";
3
3
  import { Writable } from "node:stream";
4
4
  import { pipeline } from "node:stream/promises";
5
5
 
6
- describe("generateTrack (updated with Mediabunny)", () => {
7
- test("should generate video track using Mediabunny backend", async () => {
6
+ describe("generateTrack", () => {
7
+ test("should generate video track", async () => {
8
8
  const trackStream = await generateTrackFromPath("test-assets/10s-bars.mp4", 1);
9
9
 
10
10
  // Collect the generated track data
@@ -31,7 +31,7 @@ describe("generateTrack (updated with Mediabunny)", () => {
31
31
  console.log(`Generated ${totalSize} bytes for video track`);
32
32
  }, 15000);
33
33
 
34
- test("should generate audio track using Mediabunny backend", async () => {
34
+ test("should generate audio track", async () => {
35
35
  const trackStream = await generateTrackFromPath("test-assets/10s-bars.mp4", 2);
36
36
 
37
37
  // Collect the generated track data
@@ -1,17 +1,17 @@
1
1
  import { idempotentTask } from "../idempotentTask.js";
2
2
  import debug from "debug";
3
3
  import { basename } from "node:path";
4
- import { generateTrackFromPathMediabunny } from "../generateTrackMediabunny.js";
4
+ import { generateSingleTrackFromPath } from "../generateSingleTrack.js";
5
5
 
6
6
  export const generateTrackFromPath = async (
7
7
  absolutePath: string,
8
8
  trackId: number,
9
9
  ) => {
10
10
  const log = debug("ef:generateTrackFragment");
11
- log(`Generating track ${trackId} for ${absolutePath} using Mediabunny`);
11
+ log(`Generating track ${trackId} for ${absolutePath}`);
12
12
 
13
- // Use the new Mediabunny-based implementation
14
- const result = await generateTrackFromPathMediabunny(absolutePath, trackId);
13
+ // Use the single-track implementation
14
+ const result = await generateSingleTrackFromPath(absolutePath, trackId);
15
15
 
16
16
  // Return just the stream for compatibility with existing API
17
17
  return result.stream;
@@ -1,8 +1,8 @@
1
1
  import { test, describe, assert } from "vitest";
2
2
  import { generateTrackFragmentIndexFromPath } from "./generateTrackFragmentIndex";
3
3
 
4
- describe("generateTrackFragmentIndex (updated with Mediabunny)", () => {
5
- test("should generate fragment index using Mediabunny backend", async () => {
4
+ describe("generateTrackFragmentIndex", () => {
5
+ test("should generate fragment index", async () => {
6
6
  const fragmentIndex = await generateTrackFragmentIndexFromPath("test-assets/10s-bars.mp4");
7
7
 
8
8
  // Should have multiple tracks
@@ -2,7 +2,7 @@ import { idempotentTask } from "../idempotentTask.js";
2
2
  import debug from "debug";
3
3
  import { basename } from "node:path";
4
4
  import { Probe } from "../Probe.js";
5
- import { generateTrackFragmentIndexMediabunny } from "../generateTrackFragmentIndexMediabunny.js";
5
+ import { generateFragmentIndex } from "../generateFragmentIndex.js";
6
6
  import type { TrackFragmentIndex } from "../Probe.js";
7
7
 
8
8
  export const generateTrackFragmentIndexFromPath = async (
@@ -51,7 +51,7 @@ export const generateTrackFragmentIndexFromPath = async (
51
51
  const trackStream = probe.createTrackReadstream(streamIndex);
52
52
  const trackIdMapping = { 1: trackId }; // Map single-track ID 1 to original track ID
53
53
 
54
- const singleTrackIndexes = await generateTrackFragmentIndexMediabunny(
54
+ const singleTrackIndexes = await generateFragmentIndex(
55
55
  trackStream,
56
56
  startTimeOffsetMs,
57
57
  trackIdMapping