@editframe/assets 0.16.8-beta.0 → 0.18.3-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.
package/dist/memoize.js CHANGED
@@ -1,14 +1,11 @@
1
+ /** method decorator to memoize the value of a getter */
1
2
  const memoize = (_target, _propertyKey, descriptor) => {
2
- const get = descriptor.get;
3
- if (!get) return;
4
- const memoized = /* @__PURE__ */ new WeakMap();
5
- descriptor.get = function() {
6
- if (!memoized.has(this)) {
7
- memoized.set(this, get.call(this));
8
- }
9
- return memoized.get(this);
10
- };
11
- };
12
- export {
13
- memoize
3
+ const get = descriptor.get;
4
+ if (!get) return;
5
+ const memoized = /* @__PURE__ */ new WeakMap();
6
+ descriptor.get = function() {
7
+ if (!memoized.has(this)) memoized.set(this, get.call(this));
8
+ return memoized.get(this);
9
+ };
14
10
  };
11
+ export { memoize };
@@ -1,21 +1,19 @@
1
1
  import { Writable } from "node:stream";
2
2
  const mp4FileWritable = (mp4File) => {
3
- let arrayBufferStart = 0;
4
- return new Writable({
5
- write: (chunk, _encoding, callback) => {
6
- const mp4BoxBuffer = chunk.buffer;
7
- mp4BoxBuffer.fileStart = arrayBufferStart;
8
- arrayBufferStart += chunk.length;
9
- mp4File.appendBuffer(mp4BoxBuffer, false);
10
- callback();
11
- },
12
- final: (callback) => {
13
- mp4File.flush();
14
- mp4File.processSamples(true);
15
- callback();
16
- }
17
- });
18
- };
19
- export {
20
- mp4FileWritable
3
+ let arrayBufferStart = 0;
4
+ return new Writable({
5
+ write: (chunk, _encoding, callback) => {
6
+ const mp4BoxBuffer = chunk.buffer;
7
+ mp4BoxBuffer.fileStart = arrayBufferStart;
8
+ arrayBufferStart += chunk.length;
9
+ mp4File.appendBuffer(mp4BoxBuffer, false);
10
+ callback();
11
+ },
12
+ final: (callback) => {
13
+ mp4File.flush();
14
+ mp4File.processSamples(true);
15
+ callback();
16
+ }
17
+ });
21
18
  };
19
+ export { mp4FileWritable };
@@ -2,21 +2,19 @@ import { idempotentTask } from "../idempotentTask.js";
2
2
  import { createReadStream } from "node:fs";
3
3
  import path from "node:path";
4
4
  const cacheImageTask = idempotentTask({
5
- label: "image",
6
- filename: (absolutePath) => path.basename(absolutePath),
7
- runner: async (absolutePath) => {
8
- return createReadStream(absolutePath);
9
- }
5
+ label: "image",
6
+ filename: (absolutePath) => path.basename(absolutePath),
7
+ runner: async (absolutePath) => {
8
+ return createReadStream(absolutePath);
9
+ }
10
10
  });
11
11
  const cacheImage = async (cacheRoot, absolutePath) => {
12
- try {
13
- return await cacheImageTask(cacheRoot, absolutePath);
14
- } catch (error) {
15
- console.error(error);
16
- console.trace("Error caching image", error);
17
- throw error;
18
- }
19
- };
20
- export {
21
- cacheImage
12
+ try {
13
+ return await cacheImageTask(cacheRoot, absolutePath);
14
+ } catch (error) {
15
+ console.error(error);
16
+ console.trace("Error caching image", error);
17
+ throw error;
18
+ }
22
19
  };
20
+ export { cacheImage };
@@ -1 +0,0 @@
1
- export {};
@@ -1,30 +1,27 @@
1
- import { basename } from "node:path";
2
- import { promisify } from "node:util";
3
- import { exec } from "node:child_process";
4
- import debug from "debug";
5
1
  import { idempotentTask } from "../idempotentTask.js";
2
+ import debug from "debug";
3
+ import { exec } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { basename } from "node:path";
6
6
  const execPromise = promisify(exec);
7
7
  const log = debug("ef:generateCaptions");
8
8
  const generateCaptionDataFromPath = async (absolutePath) => {
9
- const command = `whisper_timestamped --language en --efficient --output_format vtt ${absolutePath}`;
10
- log(`Running command: ${command}`);
11
- const { stdout } = await execPromise(command);
12
- return stdout;
9
+ const command = `whisper_timestamped --language en --efficient --output_format vtt ${absolutePath}`;
10
+ log(`Running command: ${command}`);
11
+ const { stdout } = await execPromise(command);
12
+ return stdout;
13
13
  };
14
14
  const generateCaptionDataTask = idempotentTask({
15
- label: "captions",
16
- filename: (absolutePath) => `${basename(absolutePath)}.captions.json`,
17
- runner: generateCaptionDataFromPath
15
+ label: "captions",
16
+ filename: (absolutePath) => `${basename(absolutePath)}.captions.json`,
17
+ runner: generateCaptionDataFromPath
18
18
  });
19
19
  const findOrCreateCaptions = async (cacheRoot, absolutePath) => {
20
- try {
21
- return await generateCaptionDataTask(cacheRoot, absolutePath);
22
- } catch (error) {
23
- console.trace("Error finding or creating captions", error);
24
- throw error;
25
- }
26
- };
27
- export {
28
- findOrCreateCaptions,
29
- generateCaptionDataFromPath
20
+ try {
21
+ return await generateCaptionDataTask(cacheRoot, absolutePath);
22
+ } catch (error) {
23
+ console.trace("Error finding or creating captions", error);
24
+ throw error;
25
+ }
30
26
  };
27
+ export { findOrCreateCaptions, generateCaptionDataFromPath };
@@ -1,72 +1,54 @@
1
- import { idempotentTask } from "../idempotentTask.js";
2
1
  import { MP4File } from "../MP4File.js";
3
- import debug from "debug";
2
+ import { Probe } from "../Probe.js";
3
+ import { idempotentTask } from "../idempotentTask.js";
4
4
  import { mp4FileWritable } from "../mp4FileWritable.js";
5
- import { PassThrough } from "node:stream";
5
+ import debug from "debug";
6
6
  import { basename } from "node:path";
7
- import { Probe } from "../Probe.js";
7
+ import { PassThrough } from "node:stream";
8
8
  const generateTrackFromPath = async (absolutePath, trackId) => {
9
- const log = debug("ef:generateTrackFragment");
10
- const probe = await Probe.probePath(absolutePath);
11
- const readStream = probe.createConformingReadstream();
12
- const mp4File = new MP4File();
13
- const trackStream = new PassThrough();
14
- log(`Generating track for ${absolutePath}`);
15
- readStream.pipe(mp4FileWritable(mp4File));
16
- (async () => {
17
- try {
18
- let bytesWritten = 0;
19
- for await (const fragment of mp4File.fragmentIterator()) {
20
- log("Writing fragment", fragment);
21
- if (fragment.track === trackId) {
22
- const written = trackStream.write(
23
- Buffer.from(fragment.data),
24
- "binary"
25
- );
26
- if (!written) {
27
- log(`Waiting for drain for track ${trackId}`);
28
- await new Promise((resolve) => trackStream.once("drain", resolve));
29
- }
30
- bytesWritten += fragment.data.byteLength;
31
- }
32
- if (!readStream.readableEnded) {
33
- await Promise.race([
34
- new Promise((resolve) => readStream.once("end", resolve)),
35
- new Promise((resolve) => readStream.once("data", resolve))
36
- ]);
37
- }
38
- }
39
- trackStream.end();
40
- } catch (error) {
41
- trackStream.destroy(error);
42
- }
43
- })();
44
- return trackStream;
9
+ const log = debug("ef:generateTrackFragment");
10
+ const probe = await Probe.probePath(absolutePath);
11
+ const readStream = probe.createConformingReadstream();
12
+ const mp4File = new MP4File();
13
+ const trackStream = new PassThrough();
14
+ log(`Generating track for ${absolutePath}`);
15
+ readStream.pipe(mp4FileWritable(mp4File));
16
+ (async () => {
17
+ try {
18
+ let bytesWritten = 0;
19
+ for await (const fragment of mp4File.fragmentIterator()) {
20
+ log("Writing fragment", fragment);
21
+ if (fragment.track === trackId) {
22
+ const written = trackStream.write(Buffer.from(fragment.data), "binary");
23
+ if (!written) {
24
+ log(`Waiting for drain for track ${trackId}`);
25
+ await new Promise((resolve) => trackStream.once("drain", resolve));
26
+ }
27
+ bytesWritten += fragment.data.byteLength;
28
+ }
29
+ if (!readStream.readableEnded) await Promise.race([new Promise((resolve) => readStream.once("end", resolve)), new Promise((resolve) => readStream.once("data", resolve))]);
30
+ }
31
+ trackStream.end();
32
+ } catch (error) {
33
+ trackStream.destroy(error);
34
+ }
35
+ })();
36
+ return trackStream;
45
37
  };
46
38
  const generateTrackTask = idempotentTask({
47
- label: "track",
48
- filename: (absolutePath, trackId) => `${basename(absolutePath)}.track-${trackId}.mp4`,
49
- runner: generateTrackFromPath
39
+ label: "track",
40
+ filename: (absolutePath, trackId) => `${basename(absolutePath)}.track-${trackId}.mp4`,
41
+ runner: generateTrackFromPath
50
42
  });
51
43
  const generateTrack = async (cacheRoot, absolutePath, url) => {
52
- try {
53
- const trackId = new URL(`http://localhost${url}`).searchParams.get(
54
- "trackId"
55
- );
56
- if (trackId === null) {
57
- throw new Error(
58
- "No trackId provided. IT must be specified in the query string: ?trackId=0"
59
- );
60
- }
61
- return await generateTrackTask(cacheRoot, absolutePath, Number(trackId));
62
- } catch (error) {
63
- console.error(error);
64
- console.trace("Error generating track", error);
65
- throw error;
66
- }
67
- };
68
- export {
69
- generateTrack,
70
- generateTrackFromPath,
71
- generateTrackTask
44
+ try {
45
+ const trackId = new URL(`http://localhost${url}`).searchParams.get("trackId");
46
+ if (trackId === null) throw new Error("No trackId provided. IT must be specified in the query string: ?trackId=0");
47
+ return await generateTrackTask(cacheRoot, absolutePath, Number(trackId));
48
+ } catch (error) {
49
+ console.error(error);
50
+ console.trace("Error generating track", error);
51
+ throw error;
52
+ }
72
53
  };
54
+ export { generateTrack, generateTrackFromPath };
@@ -1,110 +1,114 @@
1
- import { idempotentTask } from "../idempotentTask.js";
2
1
  import { MP4File } from "../MP4File.js";
3
- import debug from "debug";
2
+ import { Probe } from "../Probe.js";
3
+ import { idempotentTask } from "../idempotentTask.js";
4
4
  import { mp4FileWritable } from "../mp4FileWritable.js";
5
+ import debug from "debug";
5
6
  import { basename } from "node:path";
6
- import { Probe } from "../Probe.js";
7
7
  const generateTrackFragmentIndexFromPath = async (absolutePath) => {
8
- const log = debug("ef:generateTrackFragment");
9
- const probe = await Probe.probePath(absolutePath);
10
- const readStream = probe.createConformingReadstream();
11
- const mp4File = new MP4File();
12
- log(`Generating track fragment index for ${absolutePath}`);
13
- readStream.pipe(mp4FileWritable(mp4File));
14
- await new Promise((resolve, reject) => {
15
- readStream.on("end", resolve);
16
- readStream.on("error", reject);
17
- });
18
- const trackFragmentIndexes = {};
19
- const trackByteOffsets = {};
20
- for await (const fragment of mp4File.fragmentIterator()) {
21
- const track = mp4File.getInfo().tracks.find((track2) => track2.id === fragment.track);
22
- if (!track) {
23
- throw new Error("Track not found");
24
- }
25
- if (fragment.segment === "init") {
26
- trackByteOffsets[fragment.track] = fragment.data.byteLength;
27
- if (track?.type === "video") {
28
- const videoTrack = mp4File.getInfo().videoTracks.find((track2) => track2.id === fragment.track);
29
- if (!videoTrack) {
30
- throw new Error("Video track not found");
31
- }
32
- trackFragmentIndexes[fragment.track] = {
33
- track: fragment.track,
34
- type: "video",
35
- width: videoTrack.video.width,
36
- height: videoTrack.video.height,
37
- timescale: track.timescale,
38
- sample_count: videoTrack.nb_samples,
39
- codec: videoTrack.codec,
40
- duration: videoTrack.duration,
41
- initSegment: {
42
- offset: 0,
43
- size: fragment.data.byteLength
44
- },
45
- segments: []
46
- };
47
- }
48
- if (track?.type === "audio") {
49
- const audioTrack = mp4File.getInfo().audioTracks.find((track2) => track2.id === fragment.track);
50
- if (!audioTrack) {
51
- throw new Error("Audio track not found");
52
- }
53
- trackFragmentIndexes[fragment.track] = {
54
- track: fragment.track,
55
- type: "audio",
56
- channel_count: audioTrack.audio.channel_count,
57
- sample_rate: audioTrack.audio.sample_rate,
58
- sample_size: audioTrack.audio.sample_size,
59
- sample_count: audioTrack.nb_samples,
60
- timescale: track.timescale,
61
- codec: audioTrack.codec,
62
- duration: audioTrack.duration,
63
- initSegment: {
64
- offset: 0,
65
- size: fragment.data.byteLength
66
- },
67
- segments: []
68
- };
69
- }
70
- } else {
71
- const fragmentIndex = trackFragmentIndexes[fragment.track];
72
- if (trackByteOffsets[fragment.track] === void 0) {
73
- throw new Error("Fragment index not found");
74
- }
75
- if (!fragmentIndex) {
76
- throw new Error("Fragment index not found");
77
- }
78
- fragmentIndex.duration += fragment.duration;
79
- fragmentIndex.segments.push({
80
- cts: fragment.cts,
81
- dts: fragment.dts,
82
- duration: fragment.duration,
83
- offset: trackByteOffsets[fragment.track],
84
- size: fragment.data.byteLength
85
- });
86
- trackByteOffsets[fragment.track] += fragment.data.byteLength;
87
- }
88
- }
89
- return trackFragmentIndexes;
8
+ const log = debug("ef:generateTrackFragment");
9
+ const probe = await Probe.probePath(absolutePath);
10
+ let startTimeOffsetMs;
11
+ if (probe.format.start_time && Number(probe.format.start_time) !== 0) {
12
+ startTimeOffsetMs = Number(probe.format.start_time) * 1e3;
13
+ log(`Extracted format start_time offset: ${probe.format.start_time}s (${startTimeOffsetMs}ms)`);
14
+ } else {
15
+ const videoStream = probe.streams.find((stream) => stream.codec_type === "video");
16
+ if (videoStream && videoStream.start_time && Number(videoStream.start_time) !== 0) {
17
+ startTimeOffsetMs = Number(videoStream.start_time) * 1e3;
18
+ log(`Extracted video stream start_time offset: ${videoStream.start_time}s (${startTimeOffsetMs}ms)`);
19
+ } else log("No format/stream timing offset found - will detect from composition time");
20
+ }
21
+ const readStream = probe.createConformingReadstream();
22
+ const mp4File = new MP4File();
23
+ log(`Generating track fragment index for ${absolutePath}`);
24
+ readStream.pipe(mp4FileWritable(mp4File));
25
+ await new Promise((resolve, reject) => {
26
+ readStream.on("end", resolve);
27
+ readStream.on("error", reject);
28
+ });
29
+ const trackFragmentIndexes = {};
30
+ const trackByteOffsets = {};
31
+ for await (const fragment of mp4File.fragmentIterator()) {
32
+ const track = mp4File.getInfo().tracks.find((track$1) => track$1.id === fragment.track);
33
+ if (!track) throw new Error("Track not found");
34
+ if (fragment.segment === "init") {
35
+ trackByteOffsets[fragment.track] = fragment.data.byteLength;
36
+ if (track?.type === "video") {
37
+ const videoTrack = mp4File.getInfo().videoTracks.find((track$1) => track$1.id === fragment.track);
38
+ if (!videoTrack) throw new Error("Video track not found");
39
+ trackFragmentIndexes[fragment.track] = {
40
+ track: fragment.track,
41
+ type: "video",
42
+ width: videoTrack.video.width,
43
+ height: videoTrack.video.height,
44
+ timescale: track.timescale,
45
+ sample_count: videoTrack.nb_samples,
46
+ codec: videoTrack.codec,
47
+ duration: videoTrack.duration,
48
+ startTimeOffsetMs,
49
+ initSegment: {
50
+ offset: 0,
51
+ size: fragment.data.byteLength
52
+ },
53
+ segments: []
54
+ };
55
+ }
56
+ if (track?.type === "audio") {
57
+ const audioTrack = mp4File.getInfo().audioTracks.find((track$1) => track$1.id === fragment.track);
58
+ if (!audioTrack) throw new Error("Audio track not found");
59
+ trackFragmentIndexes[fragment.track] = {
60
+ track: fragment.track,
61
+ type: "audio",
62
+ channel_count: audioTrack.audio.channel_count,
63
+ sample_rate: audioTrack.audio.sample_rate,
64
+ sample_size: audioTrack.audio.sample_size,
65
+ sample_count: audioTrack.nb_samples,
66
+ timescale: track.timescale,
67
+ codec: audioTrack.codec,
68
+ duration: audioTrack.duration,
69
+ initSegment: {
70
+ offset: 0,
71
+ size: fragment.data.byteLength
72
+ },
73
+ segments: []
74
+ };
75
+ }
76
+ } else {
77
+ const fragmentIndex = trackFragmentIndexes[fragment.track];
78
+ if (trackByteOffsets[fragment.track] === void 0) throw new Error("Fragment index not found");
79
+ if (!fragmentIndex) throw new Error("Fragment index not found");
80
+ if (fragmentIndex.type === "video" && fragmentIndex.segments.length === 0 && fragmentIndex.startTimeOffsetMs === void 0 && fragment.cts > fragment.dts) {
81
+ const compositionOffsetMs = (fragment.cts - fragment.dts) / fragmentIndex.timescale * 1e3;
82
+ fragmentIndex.startTimeOffsetMs = compositionOffsetMs;
83
+ log(`Detected composition time offset from first video segment: ${compositionOffsetMs}ms (CTS=${fragment.cts}, DTS=${fragment.dts}, timescale=${fragmentIndex.timescale})`);
84
+ }
85
+ fragmentIndex.duration += fragment.duration;
86
+ fragmentIndex.segments.push({
87
+ cts: fragment.cts,
88
+ dts: fragment.dts,
89
+ duration: fragment.duration,
90
+ offset: trackByteOffsets[fragment.track],
91
+ size: fragment.data.byteLength
92
+ });
93
+ trackByteOffsets[fragment.track] += fragment.data.byteLength;
94
+ }
95
+ }
96
+ return trackFragmentIndexes;
90
97
  };
91
98
  const generateTrackFragmentIndexTask = idempotentTask({
92
- label: "trackFragmentIndex",
93
- filename: (absolutePath) => `${basename(absolutePath)}.tracks.json`,
94
- runner: async (absolutePath) => {
95
- const index = await generateTrackFragmentIndexFromPath(absolutePath);
96
- return JSON.stringify(index, null, 2);
97
- }
99
+ label: "trackFragmentIndex",
100
+ filename: (absolutePath) => `${basename(absolutePath)}.tracks.json`,
101
+ runner: async (absolutePath) => {
102
+ const index = await generateTrackFragmentIndexFromPath(absolutePath);
103
+ return JSON.stringify(index, null, 2);
104
+ }
98
105
  });
99
106
  const generateTrackFragmentIndex = async (cacheRoot, absolutePath) => {
100
- try {
101
- return await generateTrackFragmentIndexTask(cacheRoot, absolutePath);
102
- } catch (error) {
103
- console.trace("Error generating track fragment index", error);
104
- throw error;
105
- }
106
- };
107
- export {
108
- generateTrackFragmentIndex,
109
- generateTrackFragmentIndexFromPath
107
+ try {
108
+ return await generateTrackFragmentIndexTask(cacheRoot, absolutePath);
109
+ } catch (error) {
110
+ console.trace("Error generating track fragment index", error);
111
+ throw error;
112
+ }
110
113
  };
114
+ export { generateTrackFragmentIndex, generateTrackFragmentIndexFromPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/assets",
3
- "version": "0.16.8-beta.0",
3
+ "version": "0.18.3-beta.0",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
@@ -39,15 +39,15 @@
39
39
  "dependencies": {
40
40
  "debug": "^4.3.5",
41
41
  "mp4box": "^0.5.2",
42
- "zod": "^3.23.8",
43
- "ora": "^8.0.1"
42
+ "ora": "^8.0.1",
43
+ "zod": "^3.23.8"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/dom-webcodecs": "^0.1.11",
47
47
  "@types/node": "^20.14.13",
48
48
  "rollup-plugin-tsconfig-paths": "^1.5.2",
49
49
  "typescript": "^5.5.4",
50
- "vite-plugin-dts": "^4.0.3",
50
+ "vite-plugin-dts": "^4.5.4",
51
51
  "vite-tsconfig-paths": "^4.3.2"
52
52
  }
53
53
  }
@@ -10,6 +10,25 @@ export const generateTrackFragmentIndexFromPath = async (
10
10
  ) => {
11
11
  const log = debug("ef:generateTrackFragment");
12
12
  const probe = await Probe.probePath(absolutePath);
13
+
14
+ // Extract timing offset from probe metadata (same logic as processISOBMFF.ts)
15
+ let startTimeOffsetMs: number | undefined;
16
+
17
+ // First check format-level start_time
18
+ if (probe.format.start_time && Number(probe.format.start_time) !== 0) {
19
+ startTimeOffsetMs = Number(probe.format.start_time) * 1000;
20
+ log(`Extracted format start_time offset: ${probe.format.start_time}s (${startTimeOffsetMs}ms)`);
21
+ } else {
22
+ // Check for video stream start_time (more common)
23
+ const videoStream = probe.streams.find(stream => stream.codec_type === 'video');
24
+ if (videoStream && videoStream.start_time && Number(videoStream.start_time) !== 0) {
25
+ startTimeOffsetMs = Number(videoStream.start_time) * 1000;
26
+ log(`Extracted video stream start_time offset: ${videoStream.start_time}s (${startTimeOffsetMs}ms)`);
27
+ } else {
28
+ log("No format/stream timing offset found - will detect from composition time");
29
+ }
30
+ }
31
+
13
32
  const readStream = probe.createConformingReadstream();
14
33
 
15
34
  const mp4File = new MP4File();
@@ -50,6 +69,7 @@ export const generateTrackFragmentIndexFromPath = async (
50
69
  sample_count: videoTrack.nb_samples,
51
70
  codec: videoTrack.codec,
52
71
  duration: videoTrack.duration,
72
+ startTimeOffsetMs: startTimeOffsetMs, // Add FFmpeg start_time offset
53
73
  initSegment: {
54
74
  offset: 0,
55
75
  size: fragment.data.byteLength,
@@ -89,6 +109,18 @@ export const generateTrackFragmentIndexFromPath = async (
89
109
  if (!fragmentIndex) {
90
110
  throw new Error("Fragment index not found");
91
111
  }
112
+
113
+ // Detect composition time offset from first video segment if no timing offset was found from metadata
114
+ if (fragmentIndex.type === "video" &&
115
+ fragmentIndex.segments.length === 0 &&
116
+ fragmentIndex.startTimeOffsetMs === undefined &&
117
+ fragment.cts > fragment.dts) {
118
+ // Calculate composition time offset in milliseconds
119
+ const compositionOffsetMs = ((fragment.cts - fragment.dts) / fragmentIndex.timescale) * 1000;
120
+ fragmentIndex.startTimeOffsetMs = compositionOffsetMs;
121
+ log(`Detected composition time offset from first video segment: ${compositionOffsetMs}ms (CTS=${fragment.cts}, DTS=${fragment.dts}, timescale=${fragmentIndex.timescale})`);
122
+ }
123
+
92
124
  fragmentIndex.duration += fragment.duration;
93
125
  fragmentIndex.segments.push({
94
126
  cts: fragment.cts,