@editframe/assets 0.17.6-beta.0 → 0.18.7-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/Probe.d.ts +441 -29
- package/dist/Probe.js +156 -21
- package/dist/VideoRenderOptions.d.ts +27 -5
- package/dist/VideoRenderOptions.js +1 -1
- package/dist/generateTrackFragmentIndexMediabunny.d.ts +3 -0
- package/dist/generateTrackFragmentIndexMediabunny.js +343 -0
- package/dist/generateTrackMediabunny.d.ts +8 -0
- package/dist/generateTrackMediabunny.js +69 -0
- package/dist/idempotentTask.js +81 -48
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/tasks/cacheRemoteAsset.d.ts +0 -1
- package/dist/tasks/findOrCreateCaptions.js +1 -1
- package/dist/tasks/generateTrack.d.ts +1 -2
- package/dist/tasks/generateTrack.js +5 -32
- package/dist/tasks/generateTrackFragmentIndex.js +22 -69
- package/dist/truncateDecimal.d.ts +1 -0
- package/dist/truncateDecimal.js +5 -0
- package/package.json +2 -14
- package/src/tasks/generateTrack.test.ts +90 -0
- package/src/tasks/generateTrack.ts +7 -48
- package/src/tasks/generateTrackFragmentIndex.test.ts +115 -0
- package/src/tasks/generateTrackFragmentIndex.ts +46 -85
- package/types.json +1 -1
- package/dist/DecoderManager.d.ts +0 -62
- package/dist/DecoderManager.js +0 -114
- package/dist/EncodedAsset.d.ts +0 -143
- package/dist/EncodedAsset.js +0 -443
- package/dist/FrameBuffer.d.ts +0 -62
- package/dist/FrameBuffer.js +0 -89
- package/dist/MP4File.d.ts +0 -37
- package/dist/MP4File.js +0 -209
- package/dist/MP4SampleAnalyzer.d.ts +0 -59
- package/dist/MP4SampleAnalyzer.js +0 -119
- package/dist/SeekStrategy.d.ts +0 -82
- package/dist/SeekStrategy.js +0 -101
- package/dist/memoize.js +0 -11
- package/dist/mp4FileWritable.d.ts +0 -3
- package/dist/mp4FileWritable.js +0 -19
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { test, describe, assert } from "vitest";
|
|
2
|
+
import { generateTrackFragmentIndexFromPath } from "./generateTrackFragmentIndex";
|
|
3
|
+
|
|
4
|
+
describe("generateTrackFragmentIndex (updated with Mediabunny)", () => {
|
|
5
|
+
test("should generate fragment index using Mediabunny backend", 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
|
+
console.log(`Track ${trackId} (${track.type}): ${track.segments.length} segments, ${track.sample_count} samples`);
|
|
51
|
+
}
|
|
52
|
+
}, 20000);
|
|
53
|
+
|
|
54
|
+
test("should handle single track files", async () => {
|
|
55
|
+
const fragmentIndex = await generateTrackFragmentIndexFromPath("test-assets/frame-count.mp4");
|
|
56
|
+
|
|
57
|
+
const trackIds = Object.keys(fragmentIndex).map(Number);
|
|
58
|
+
assert.equal(trackIds.length, 1, "Should have exactly one track");
|
|
59
|
+
|
|
60
|
+
const track = fragmentIndex[trackIds[0]!]!;
|
|
61
|
+
assert.equal(track.type, "video", "Should be video track");
|
|
62
|
+
assert.isAbove(track.segments.length, 0, "Should have segments");
|
|
63
|
+
|
|
64
|
+
console.log(`Single track: ${track.segments.length} segments, ${track.sample_count} samples`);
|
|
65
|
+
}, 15000);
|
|
66
|
+
|
|
67
|
+
test("should generate consistent results with original implementation", async () => {
|
|
68
|
+
// Test that the new implementation produces similar structure to the old one
|
|
69
|
+
const fragmentIndex = await generateTrackFragmentIndexFromPath("test-assets/bars-n-tone.mp4");
|
|
70
|
+
|
|
71
|
+
const trackIds = Object.keys(fragmentIndex).map(Number);
|
|
72
|
+
assert.equal(trackIds.length, 2, "Should have video and audio tracks");
|
|
73
|
+
|
|
74
|
+
// Should have both video and audio
|
|
75
|
+
const videoTrack = Object.values(fragmentIndex).find(t => t.type === 'video');
|
|
76
|
+
const audioTrack = Object.values(fragmentIndex).find(t => t.type === 'audio');
|
|
77
|
+
|
|
78
|
+
assert.exists(videoTrack, "Should have video track");
|
|
79
|
+
assert.exists(audioTrack, "Should have audio track");
|
|
80
|
+
|
|
81
|
+
// Video track checks
|
|
82
|
+
assert.isAbove(videoTrack.width, 0, "Video should have width");
|
|
83
|
+
assert.isAbove(videoTrack.height, 0, "Video should have height");
|
|
84
|
+
assert.isAbove(videoTrack.segments.length, 0, "Video should have segments");
|
|
85
|
+
|
|
86
|
+
// Audio track checks
|
|
87
|
+
assert.isAbove(audioTrack.channel_count, 0, "Audio should have channels");
|
|
88
|
+
assert.isAbove(audioTrack.sample_rate, 0, "Audio should have sample rate");
|
|
89
|
+
assert.isAbove(audioTrack.segments.length, 0, "Audio should have segments");
|
|
90
|
+
|
|
91
|
+
console.log(`Consistent results: video ${videoTrack.segments.length} segments, audio ${audioTrack.segments.length} segments`);
|
|
92
|
+
}, 20000);
|
|
93
|
+
|
|
94
|
+
test("should preserve timing offset detection", async () => {
|
|
95
|
+
// Test with a file that might have timing offsets
|
|
96
|
+
const fragmentIndex = await generateTrackFragmentIndexFromPath("test-assets/frame-count.mp4");
|
|
97
|
+
|
|
98
|
+
const trackIds = Object.keys(fragmentIndex).map(Number);
|
|
99
|
+
const track = fragmentIndex[trackIds[0]!]!;
|
|
100
|
+
|
|
101
|
+
if (track.type === 'video' && track.startTimeOffsetMs !== undefined) {
|
|
102
|
+
assert.isNumber(track.startTimeOffsetMs, "Should have timing offset");
|
|
103
|
+
console.log(`Detected timing offset: ${track.startTimeOffsetMs}ms`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log("No timing offset detected (expected for this file)");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Should still have valid timing data
|
|
109
|
+
assert.isAbove(track.duration, 0, "Should have positive duration");
|
|
110
|
+
for (const segment of track.segments) {
|
|
111
|
+
assert.isAbove(segment.duration, 0, "Each segment should have positive duration");
|
|
112
|
+
}
|
|
113
|
+
}, 15000);
|
|
114
|
+
});
|
|
115
|
+
|
|
@@ -1,105 +1,66 @@
|
|
|
1
1
|
import { idempotentTask } from "../idempotentTask.js";
|
|
2
|
-
import { MP4File } from "../MP4File.js";
|
|
3
2
|
import debug from "debug";
|
|
4
|
-
import { mp4FileWritable } from "../mp4FileWritable.js";
|
|
5
3
|
import { basename } from "node:path";
|
|
6
|
-
import { Probe
|
|
4
|
+
import { Probe } from "../Probe.js";
|
|
5
|
+
import { generateTrackFragmentIndexMediabunny } from "../generateTrackFragmentIndexMediabunny.js";
|
|
6
|
+
import type { TrackFragmentIndex } from "../Probe.js";
|
|
7
7
|
|
|
8
8
|
export const generateTrackFragmentIndexFromPath = async (
|
|
9
9
|
absolutePath: string,
|
|
10
10
|
) => {
|
|
11
11
|
const log = debug("ef:generateTrackFragment");
|
|
12
12
|
const probe = await Probe.probePath(absolutePath);
|
|
13
|
-
const readStream = probe.createConformingReadstream();
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
// Extract timing offset from probe metadata (same logic as processISOBMFF.ts)
|
|
15
|
+
let startTimeOffsetMs: number | undefined;
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|
|
23
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
|
|
24
36
|
const trackFragmentIndexes: Record<number, TrackFragmentIndex> = {};
|
|
25
|
-
const trackByteOffsets: Record<number, number> = {};
|
|
26
|
-
for await (const fragment of mp4File.fragmentIterator()) {
|
|
27
|
-
const track = mp4File
|
|
28
|
-
.getInfo()
|
|
29
|
-
.tracks.find((track) => track.id === fragment.track);
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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]!;
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const videoTrack = mp4File
|
|
39
|
-
.getInfo()
|
|
40
|
-
.videoTracks.find((track) => track.id === fragment.track);
|
|
41
|
-
if (!videoTrack) {
|
|
42
|
-
throw new Error("Video track not found");
|
|
43
|
-
}
|
|
44
|
-
trackFragmentIndexes[fragment.track] = {
|
|
45
|
-
track: fragment.track,
|
|
46
|
-
type: "video",
|
|
47
|
-
width: videoTrack.video.width,
|
|
48
|
-
height: videoTrack.video.height,
|
|
49
|
-
timescale: track.timescale,
|
|
50
|
-
sample_count: videoTrack.nb_samples,
|
|
51
|
-
codec: videoTrack.codec,
|
|
52
|
-
duration: videoTrack.duration,
|
|
53
|
-
initSegment: {
|
|
54
|
-
offset: 0,
|
|
55
|
-
size: fragment.data.byteLength,
|
|
56
|
-
},
|
|
57
|
-
segments: [],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
if (track?.type === "audio") {
|
|
61
|
-
const audioTrack = mp4File
|
|
62
|
-
.getInfo()
|
|
63
|
-
.audioTracks.find((track) => track.id === fragment.track);
|
|
64
|
-
if (!audioTrack) {
|
|
65
|
-
throw new Error("Audio track not found");
|
|
66
|
-
}
|
|
67
|
-
trackFragmentIndexes[fragment.track] = {
|
|
68
|
-
track: fragment.track,
|
|
69
|
-
type: "audio",
|
|
70
|
-
channel_count: audioTrack.audio.channel_count,
|
|
71
|
-
sample_rate: audioTrack.audio.sample_rate,
|
|
72
|
-
sample_size: audioTrack.audio.sample_size,
|
|
73
|
-
sample_count: audioTrack.nb_samples,
|
|
74
|
-
timescale: track.timescale,
|
|
75
|
-
codec: audioTrack.codec,
|
|
76
|
-
duration: audioTrack.duration,
|
|
77
|
-
initSegment: {
|
|
78
|
-
offset: 0,
|
|
79
|
-
size: fragment.data.byteLength,
|
|
80
|
-
},
|
|
81
|
-
segments: [],
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
const fragmentIndex = trackFragmentIndexes[fragment.track];
|
|
86
|
-
if (trackByteOffsets[fragment.track] === undefined) {
|
|
87
|
-
throw new Error("Fragment index not found");
|
|
88
|
-
}
|
|
89
|
-
if (!fragmentIndex) {
|
|
90
|
-
throw new Error("Fragment index not found");
|
|
91
|
-
}
|
|
92
|
-
fragmentIndex.duration += fragment.duration;
|
|
93
|
-
fragmentIndex.segments.push({
|
|
94
|
-
cts: fragment.cts,
|
|
95
|
-
dts: fragment.dts,
|
|
96
|
-
duration: fragment.duration,
|
|
97
|
-
offset: trackByteOffsets[fragment.track]!,
|
|
98
|
-
size: fragment.data.byteLength,
|
|
99
|
-
});
|
|
100
|
-
trackByteOffsets[fragment.track]! += fragment.data.byteLength;
|
|
42
|
+
// Only process audio and video streams
|
|
43
|
+
if (stream.codec_type !== 'audio' && stream.codec_type !== 'video') {
|
|
44
|
+
continue;
|
|
101
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 = { 1: trackId }; // Map single-track ID 1 to original track ID
|
|
53
|
+
|
|
54
|
+
const singleTrackIndexes = await generateTrackFragmentIndexMediabunny(
|
|
55
|
+
trackStream,
|
|
56
|
+
startTimeOffsetMs,
|
|
57
|
+
trackIdMapping
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Merge the single-track index into the combined result
|
|
61
|
+
Object.assign(trackFragmentIndexes, singleTrackIndexes);
|
|
102
62
|
}
|
|
63
|
+
|
|
103
64
|
return trackFragmentIndexes;
|
|
104
65
|
};
|
|
105
66
|
|