@editframe/assets 0.26.3-beta.0 → 0.26.4-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/assets",
3
- "version": "0.26.3-beta.0",
3
+ "version": "0.26.4-beta.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,22 +0,0 @@
1
- import { idempotentTask } from "../idempotentTask.js";
2
- import { createReadStream } from "node:fs";
3
-
4
- import path from "node:path";
5
-
6
- const cacheImageTask = idempotentTask({
7
- label: "image",
8
- filename: (absolutePath: string) => path.basename(absolutePath),
9
- runner: async (absolutePath) => {
10
- return createReadStream(absolutePath);
11
- },
12
- });
13
-
14
- export const cacheImage = async (cacheRoot: string, absolutePath: string) => {
15
- try {
16
- return await cacheImageTask(cacheRoot, absolutePath);
17
- } catch (error) {
18
- console.error(error);
19
- console.trace("Error caching image", error);
20
- throw error;
21
- }
22
- };
File without changes
@@ -1,36 +0,0 @@
1
- import { basename } from "node:path";
2
- import { promisify } from "node:util";
3
- import { exec } from "node:child_process";
4
-
5
- import debug from "debug";
6
-
7
- import { idempotentTask } from "../idempotentTask.js";
8
-
9
- const execPromise = promisify(exec);
10
-
11
- const log = debug("ef:generateCaptions");
12
-
13
- export const generateCaptionDataFromPath = async (absolutePath: string) => {
14
- const command = `whisper_timestamped --language en --efficient --output_format vtt ${absolutePath}`;
15
- log(`Running command: ${command}`);
16
- const { stdout } = await execPromise(command);
17
- return stdout;
18
- };
19
-
20
- const generateCaptionDataTask = idempotentTask({
21
- label: "captions",
22
- filename: (absolutePath) => `${basename(absolutePath)}.captions.json`,
23
- runner: generateCaptionDataFromPath,
24
- });
25
-
26
- export const findOrCreateCaptions = async (
27
- cacheRoot: string,
28
- absolutePath: string,
29
- ) => {
30
- try {
31
- return await generateCaptionDataTask(cacheRoot, absolutePath);
32
- } catch (error) {
33
- console.trace("Error finding or creating captions", error);
34
- throw error;
35
- }
36
- };
@@ -1,90 +0,0 @@
1
- import { test, describe, assert } from "vitest";
2
- import { generateTrackFromPath } from "./generateTrack";
3
- import { Writable } from "node:stream";
4
- import { pipeline } from "node:stream/promises";
5
-
6
- describe("generateTrack", () => {
7
- test("should generate video track", async () => {
8
- const trackStream = await generateTrackFromPath("test-assets/10s-bars.mp4", 1);
9
-
10
- // Collect the generated track data
11
- const chunks: Buffer[] = [];
12
- const dest = new Writable({
13
- write(chunk, _encoding, callback) {
14
- chunks.push(chunk);
15
- callback();
16
- }
17
- });
18
-
19
- await pipeline(trackStream, dest);
20
-
21
- // Verify we got MP4 data
22
- assert.isAbove(chunks.length, 0, "Should generate MP4 chunks");
23
- const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
24
- assert.isAbove(totalSize, 1000, "Should generate substantial MP4 data");
25
-
26
- // Verify it's valid MP4 by checking for ftyp box
27
- const allData = Buffer.concat(chunks);
28
- const ftypIndex = allData.indexOf('ftyp');
29
- assert.isAbove(ftypIndex, -1, "Should contain ftyp box (valid MP4)");
30
-
31
- console.log(`Generated ${totalSize} bytes for video track`);
32
- }, 15000);
33
-
34
- test("should generate audio track", async () => {
35
- const trackStream = await generateTrackFromPath("test-assets/10s-bars.mp4", 2);
36
-
37
- // Collect the generated track data
38
- const chunks: Buffer[] = [];
39
- const dest = new Writable({
40
- write(chunk, _encoding, callback) {
41
- chunks.push(chunk);
42
- callback();
43
- }
44
- });
45
-
46
- await pipeline(trackStream, dest);
47
-
48
- // Verify we got MP4 data
49
- assert.isAbove(chunks.length, 0, "Should generate MP4 chunks");
50
- const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
51
- assert.isAbove(totalSize, 1000, "Should generate substantial MP4 data");
52
-
53
- // Verify it's valid MP4 by checking for ftyp box
54
- const allData = Buffer.concat(chunks);
55
- const ftypIndex = allData.indexOf('ftyp');
56
- assert.isAbove(ftypIndex, -1, "Should contain ftyp box (valid MP4)");
57
-
58
- console.log(`Generated ${totalSize} bytes for audio track`);
59
- }, 15000);
60
-
61
- test("should handle invalid track IDs gracefully", async () => {
62
- try {
63
- await generateTrackFromPath("test-assets/frame-count.mp4", 5);
64
- assert.fail("Should have thrown for invalid track ID");
65
- } catch (error) {
66
- assert.instanceOf(error, Error);
67
- assert.include(error.message, "Track 5 not found");
68
- }
69
- });
70
-
71
- test("should work with single track files", async () => {
72
- const trackStream = await generateTrackFromPath("test-assets/frame-count.mp4", 1);
73
-
74
- const chunks: Buffer[] = [];
75
- const dest = new Writable({
76
- write(chunk, _encoding, callback) {
77
- chunks.push(chunk);
78
- callback();
79
- }
80
- });
81
-
82
- await pipeline(trackStream, dest);
83
-
84
- const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
85
- assert.isAbove(totalSize, 1000, "Should generate data for single track file");
86
-
87
- console.log(`Generated ${totalSize} bytes for single track file`);
88
- }, 15000);
89
- });
90
-
@@ -1,47 +0,0 @@
1
- import { idempotentTask } from "../idempotentTask.js";
2
- import debug from "debug";
3
- import { basename } from "node:path";
4
- import { generateSingleTrackFromPath } from "../generateSingleTrack.js";
5
-
6
- export const generateTrackFromPath = async (
7
- absolutePath: string,
8
- trackId: number,
9
- ) => {
10
- const log = debug("ef:generateTrackFragment");
11
- log(`Generating track ${trackId} for ${absolutePath}`);
12
-
13
- // Use the single-track implementation
14
- const result = await generateSingleTrackFromPath(absolutePath, trackId);
15
-
16
- // Return just the stream for compatibility with existing API
17
- return result.stream;
18
- };
19
-
20
- export const generateTrackTask = idempotentTask({
21
- label: "track",
22
- filename: (absolutePath: string, trackId: number) =>
23
- `${basename(absolutePath)}.track-${trackId}.mp4`,
24
- runner: generateTrackFromPath,
25
- });
26
-
27
- export const generateTrack = async (
28
- cacheRoot: string,
29
- absolutePath: string,
30
- url: string,
31
- ) => {
32
- try {
33
- const trackId = new URL(`http://localhost${url}`).searchParams.get(
34
- "trackId",
35
- );
36
- if (trackId === null) {
37
- throw new Error(
38
- "No trackId provided. It must be specified in the query string: ?trackId=1 (for video) or ?trackId=2 (for audio)",
39
- );
40
- }
41
- return await generateTrackTask(cacheRoot, absolutePath, Number(trackId));
42
- } catch (error) {
43
- console.error(error);
44
- console.trace("Error generating track", error);
45
- throw error;
46
- }
47
- };
@@ -1,105 +0,0 @@
1
- import { test, describe, assert } from "vitest";
2
- import { generateTrackFragmentIndexFromPath } from "./generateTrackFragmentIndex";
3
-
4
- describe("generateTrackFragmentIndex", () => {
5
- test("should generate fragment index", async () => {
6
- const fragmentIndex = await generateTrackFragmentIndexFromPath("test-assets/10s-bars.mp4");
7
-
8
- // Should have multiple tracks
9
- const trackIds = Object.keys(fragmentIndex).map(Number);
10
- assert.isAbove(trackIds.length, 0, "Should have tracks");
11
-
12
- for (const trackId of trackIds) {
13
- const track = fragmentIndex[trackId]!;
14
-
15
- // Verify track structure
16
- assert.oneOf(track.type, ['video', 'audio'], `Track ${trackId} should be video or audio`);
17
- assert.isNumber(track.track, `Track ${trackId} should have track number`);
18
- assert.isNumber(track.timescale, `Track ${trackId} should have timescale`);
19
- assert.isNumber(track.duration, `Track ${trackId} should have duration`);
20
- assert.isNumber(track.sample_count, `Track ${trackId} should have sample_count`);
21
- assert.isString(track.codec, `Track ${trackId} should have codec`);
22
-
23
- // Verify init segment
24
- assert.equal(track.initSegment.offset, 0, `Track ${trackId} init should start at 0`);
25
- assert.isAbove(track.initSegment.size, 0, `Track ${trackId} init should have size`);
26
-
27
- // Verify segments
28
- assert.isArray(track.segments, `Track ${trackId} should have segments array`);
29
- assert.isAbove(track.segments.length, 0, `Track ${trackId} should have segments`);
30
-
31
- // Check each segment
32
- for (const segment of track.segments) {
33
- assert.isNumber(segment.cts, `Track ${trackId} segment should have cts`);
34
- assert.isNumber(segment.dts, `Track ${trackId} segment should have dts`);
35
- assert.isNumber(segment.duration, `Track ${trackId} segment should have duration`);
36
- assert.isNumber(segment.offset, `Track ${trackId} segment should have offset`);
37
- assert.isNumber(segment.size, `Track ${trackId} segment should have size`);
38
- }
39
-
40
- // Type-specific checks
41
- if (track.type === 'video') {
42
- assert.isNumber(track.width, `Video track ${trackId} should have width`);
43
- assert.isNumber(track.height, `Video track ${trackId} should have height`);
44
- } else if (track.type === 'audio') {
45
- assert.isNumber(track.channel_count, `Audio track ${trackId} should have channel_count`);
46
- assert.isNumber(track.sample_rate, `Audio track ${trackId} should have sample_rate`);
47
- assert.isNumber(track.sample_size, `Audio track ${trackId} should have sample_size`);
48
- }
49
- }
50
- });
51
-
52
- test("should handle single track files", async () => {
53
- const fragmentIndex = await generateTrackFragmentIndexFromPath("test-assets/frame-count.mp4");
54
-
55
- const trackIds = Object.keys(fragmentIndex).map(Number);
56
- assert.equal(trackIds.length, 1, "Should have exactly one track");
57
-
58
- const track = fragmentIndex[trackIds[0]!]!;
59
- assert.equal(track.type, "video", "Should be video track");
60
- assert.isAbove(track.segments.length, 0, "Should have segments");
61
- });
62
-
63
- test("should generate consistent results with original implementation", async () => {
64
- // Test that the new implementation produces similar structure to the old one
65
- const fragmentIndex = await generateTrackFragmentIndexFromPath("test-assets/bars-n-tone.mp4");
66
-
67
- const trackIds = Object.keys(fragmentIndex).map(Number);
68
- assert.equal(trackIds.length, 2, "Should have video and audio tracks");
69
-
70
- // Should have both video and audio
71
- const videoTrack = Object.values(fragmentIndex).find(t => t.type === 'video');
72
- const audioTrack = Object.values(fragmentIndex).find(t => t.type === 'audio');
73
-
74
- assert.exists(videoTrack, "Should have video track");
75
- assert.exists(audioTrack, "Should have audio track");
76
-
77
- // Video track checks
78
- assert.isAbove(videoTrack.width, 0, "Video should have width");
79
- assert.isAbove(videoTrack.height, 0, "Video should have height");
80
- assert.isAbove(videoTrack.segments.length, 0, "Video should have segments");
81
-
82
- // Audio track checks
83
- assert.isAbove(audioTrack.channel_count, 0, "Audio should have channels");
84
- assert.isAbove(audioTrack.sample_rate, 0, "Audio should have sample rate");
85
- assert.isAbove(audioTrack.segments.length, 0, "Audio should have segments");
86
- }, 20000);
87
-
88
- test("should preserve timing offset detection", async () => {
89
- // Test with a file that might have timing offsets
90
- const fragmentIndex = await generateTrackFragmentIndexFromPath("test-assets/frame-count.mp4");
91
-
92
- const trackIds = Object.keys(fragmentIndex).map(Number);
93
- const track = fragmentIndex[trackIds[0]!]!;
94
-
95
- assert.equal(track.startTimeOffsetMs, 200);
96
- assert.equal(track.type, "video");
97
-
98
- // Should still have valid timing data
99
- assert.isAbove(track.duration, 0, "Should have positive duration");
100
- for (const segment of track.segments) {
101
- assert.isAbove(segment.duration, 0, "Each segment should have positive duration");
102
- }
103
- }, 15000);
104
- });
105
-
@@ -1,86 +0,0 @@
1
- import { idempotentTask } from "../idempotentTask.js";
2
- import debug from "debug";
3
- import { basename } from "node:path";
4
- import { Probe } from "../Probe.js";
5
- import { generateFragmentIndex } from "../generateFragmentIndex.js";
6
- import type { TrackFragmentIndex } from "../Probe.js";
7
-
8
- export const generateTrackFragmentIndexFromPath = async (
9
- absolutePath: string,
10
- ) => {
11
- const log = debug("ef:generateTrackFragment");
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
-
32
- log(`Generating track fragment index for ${absolutePath} using single-track approach`);
33
-
34
- // FIXED: Generate fragment indexes from individual single-track files
35
- // This ensures byte offsets match the actual single-track files that clients will request
36
- const trackFragmentIndexes: Record<number, TrackFragmentIndex> = {};
37
-
38
- // Process each audio/video stream as a separate track
39
- for (let streamIndex = 0; streamIndex < probe.streams.length; streamIndex++) {
40
- const stream = probe.streams[streamIndex]!;
41
-
42
- // Only process audio and video streams
43
- if (stream.codec_type !== 'audio' && stream.codec_type !== 'video') {
44
- continue;
45
- }
46
-
47
- const trackId = streamIndex + 1; // Convert to 1-based track ID
48
- log(`Processing track ${trackId} (${stream.codec_type})`);
49
-
50
- // Generate single-track file and its fragment index
51
- const trackStream = probe.createTrackReadstream(streamIndex);
52
- const trackIdMapping = { 0: trackId }; // Map single-track stream index 0 to original track ID
53
-
54
- const singleTrackIndexes = await generateFragmentIndex(
55
- trackStream,
56
- startTimeOffsetMs,
57
- trackIdMapping
58
- );
59
-
60
- // Merge the single-track index into the combined result
61
- Object.assign(trackFragmentIndexes, singleTrackIndexes);
62
- }
63
-
64
- return trackFragmentIndexes;
65
- };
66
-
67
- const generateTrackFragmentIndexTask = idempotentTask({
68
- label: "trackFragmentIndex",
69
- filename: (absolutePath) => `${basename(absolutePath)}.tracks.json`,
70
- runner: async (absolutePath: string) => {
71
- const index = await generateTrackFragmentIndexFromPath(absolutePath);
72
- return JSON.stringify(index, null, 2);
73
- },
74
- });
75
-
76
- export const generateTrackFragmentIndex = async (
77
- cacheRoot: string,
78
- absolutePath: string,
79
- ) => {
80
- try {
81
- return await generateTrackFragmentIndexTask(cacheRoot, absolutePath);
82
- } catch (error) {
83
- console.trace("Error generating track fragment index", error);
84
- throw error;
85
- }
86
- };