@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.
- package/dist/generateFragmentIndex.d.ts +3 -0
- package/dist/{generateTrackFragmentIndexMediabunny.js → generateFragmentIndex.js} +24 -16
- package/dist/generateSingleTrack.d.ts +8 -0
- package/dist/{generateTrackMediabunny.js → generateSingleTrack.js} +30 -25
- package/dist/idempotentTask.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/tasks/generateTrack.js +3 -3
- package/dist/tasks/generateTrackFragmentIndex.js +2 -2
- package/package.json +1 -1
- package/src/tasks/generateTrack.test.ts +3 -3
- package/src/tasks/generateTrack.ts +4 -4
- package/src/tasks/generateTrackFragmentIndex.test.ts +2 -2
- package/src/tasks/generateTrackFragmentIndex.ts +2 -2
- package/types.json +1 -1
- package/dist/generateTrackFragmentIndexMediabunny.d.ts +0 -3
- package/dist/generateTrackMediabunny.d.ts +0 -8
|
@@ -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:
|
|
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(
|
|
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
|
|
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
|
|
115
|
+
const generateFragmentIndex = async (inputStream, startTimeOffsetMs, trackIdMapping) => {
|
|
116
116
|
const parser = new StreamingBoxParser();
|
|
117
|
-
const
|
|
117
|
+
const chunks = [];
|
|
118
118
|
let totalSize = 0;
|
|
119
|
-
const
|
|
120
|
-
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
257
|
-
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:
|
|
315
|
-
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 {
|
|
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
|
-
|
|
8
|
-
const
|
|
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 =
|
|
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
|
|
42
|
-
label: "track-
|
|
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
|
|
45
|
+
const result = await generateSingleTrackFromPath(absolutePath, trackId);
|
|
46
46
|
const finalStream = new PassThrough();
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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.
|
|
61
|
-
|
|
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 {
|
|
74
|
+
export { generateSingleTrackFromPath };
|
package/dist/idempotentTask.js
CHANGED
|
@@ -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 {
|
|
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}
|
|
8
|
-
const result = await
|
|
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
|
|
29
|
+
const singleTrackIndexes = await generateFragmentIndex(trackStream, startTimeOffsetMs, trackIdMapping);
|
|
30
30
|
Object.assign(trackFragmentIndexes, singleTrackIndexes);
|
|
31
31
|
}
|
|
32
32
|
return trackFragmentIndexes;
|
package/package.json
CHANGED
|
@@ -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
|
|
7
|
-
test("should generate video track
|
|
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
|
|
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 {
|
|
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}
|
|
11
|
+
log(`Generating track ${trackId} for ${absolutePath}`);
|
|
12
12
|
|
|
13
|
-
// Use the
|
|
14
|
-
const result = await
|
|
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
|
|
5
|
-
test("should generate fragment index
|
|
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 {
|
|
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
|
|
54
|
+
const singleTrackIndexes = await generateFragmentIndex(
|
|
55
55
|
trackStream,
|
|
56
56
|
startTimeOffsetMs,
|
|
57
57
|
trackIdMapping
|