@editframe/vite-plugin 0.18.22-beta.0 → 0.18.23-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/forbidRelativePaths.js +4 -0
- package/dist/index.js +74 -0
- package/dist/sendTaskResult.js +53 -0
- package/package.json +3 -3
- package/dist/assets/src/tasks/cacheImage.js +0 -20
- package/dist/assets/src/tasks/findOrCreateCaptions.js +0 -27
- package/dist/assets/src/tasks/generateTrack.js +0 -27
- package/dist/assets/src/tasks/generateTrackFragmentIndex.js +0 -50
package/dist/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { forbidRelativePaths } from "./forbidRelativePaths.js";
|
|
2
|
+
import { sendTaskResult } from "./sendTaskResult.js";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import path, { join } from "node:path";
|
|
5
|
+
import { cacheImage, findOrCreateCaptions, generateTrack, generateTrackFragmentIndex, md5FilePath } from "@editframe/assets";
|
|
6
|
+
import debug from "debug";
|
|
7
|
+
const vitePluginEditframe = (options) => {
|
|
8
|
+
return {
|
|
9
|
+
name: "vite-plugin-editframe",
|
|
10
|
+
configureServer(server) {
|
|
11
|
+
server.middlewares.use(async (req, res, next) => {
|
|
12
|
+
const log = debug("ef:vite-plugin");
|
|
13
|
+
if (req.url?.startsWith("/@ef")) forbidRelativePaths(req);
|
|
14
|
+
else return next();
|
|
15
|
+
log(`Handling ${req.url}`);
|
|
16
|
+
const requestPath = req.url.replace(/^\/@ef-[^/]+\//, "");
|
|
17
|
+
const assetPath = requestPath.replace(/\?.*$/, "");
|
|
18
|
+
const absolutePath = assetPath.startsWith("http") ? assetPath : path.join(options.root, assetPath).replace("dist/", "src/");
|
|
19
|
+
options.cacheRoot = options.cacheRoot.replace("dist/", "src/");
|
|
20
|
+
const efPrefix = req.url.split("/")[1];
|
|
21
|
+
switch (efPrefix) {
|
|
22
|
+
case "@ef-clear-cache": {
|
|
23
|
+
if (req.method !== "DELETE") {
|
|
24
|
+
res.writeHead(405, { Allow: "DELETE" });
|
|
25
|
+
res.end();
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
log(`Clearing cache for ${options.cacheRoot}`);
|
|
29
|
+
await rm(join(options.cacheRoot, ".cache"), {
|
|
30
|
+
recursive: true,
|
|
31
|
+
force: true
|
|
32
|
+
});
|
|
33
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
34
|
+
res.end("Cache cleared");
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case "@ef-asset": {
|
|
38
|
+
if (req.method !== "HEAD") {
|
|
39
|
+
res.writeHead(405, { Allow: "HEAD" });
|
|
40
|
+
res.end();
|
|
41
|
+
}
|
|
42
|
+
md5FilePath(absolutePath).then((md5) => {
|
|
43
|
+
res.writeHead(200, { etag: md5 });
|
|
44
|
+
res.end();
|
|
45
|
+
}).catch(next);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "@ef-track-fragment-index": {
|
|
49
|
+
log(`Serving track fragment index for ${absolutePath}`);
|
|
50
|
+
generateTrackFragmentIndex(options.cacheRoot, absolutePath).then((taskResult) => sendTaskResult(req, res, taskResult)).catch(next);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "@ef-track": {
|
|
54
|
+
log(`Serving track for ${absolutePath}`);
|
|
55
|
+
generateTrack(options.cacheRoot, absolutePath, req.url).then((taskResult) => sendTaskResult(req, res, taskResult)).catch(next);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "@ef-captions":
|
|
59
|
+
log(`Serving captions for ${absolutePath}`);
|
|
60
|
+
findOrCreateCaptions(options.cacheRoot, absolutePath).then((taskResult) => sendTaskResult(req, res, taskResult)).catch(next);
|
|
61
|
+
break;
|
|
62
|
+
case "@ef-image":
|
|
63
|
+
log(`Serving image file ${absolutePath}`);
|
|
64
|
+
cacheImage(options.cacheRoot, absolutePath).then((taskResult) => sendTaskResult(req, res, taskResult)).catch(next);
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
log(`Unknown asset type ${efPrefix}`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
export { vitePluginEditframe };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import debug from "debug";
|
|
2
|
+
import { createReadStream, statSync } from "node:fs";
|
|
3
|
+
import mime from "mime";
|
|
4
|
+
const sendTaskResult = (req, res, taskResult) => {
|
|
5
|
+
const { cachePath, md5Sum } = taskResult;
|
|
6
|
+
const filePath = cachePath;
|
|
7
|
+
const headers = { etag: md5Sum };
|
|
8
|
+
const log = debug("ef:sendfile");
|
|
9
|
+
try {
|
|
10
|
+
log(`Sending file ${filePath}`);
|
|
11
|
+
const stats = statSync(filePath);
|
|
12
|
+
if (req.headers.range) {
|
|
13
|
+
const [x, y] = req.headers.range.replace("bytes=", "").split("-");
|
|
14
|
+
let end = Number.parseInt(y ?? "0", 10) || stats.size - 1;
|
|
15
|
+
const start = Number.parseInt(x ?? "0", 10) || 0;
|
|
16
|
+
if (end >= stats.size) end = stats.size - 1;
|
|
17
|
+
if (start >= stats.size) {
|
|
18
|
+
log("Range start is greater than file size");
|
|
19
|
+
res.setHeader("Content-Range", `bytes */${stats.size}`);
|
|
20
|
+
res.statusCode = 416;
|
|
21
|
+
return res.end();
|
|
22
|
+
}
|
|
23
|
+
res.writeHead(206, {
|
|
24
|
+
...headers,
|
|
25
|
+
"Content-Type": mime.getType(filePath) || "text/plain",
|
|
26
|
+
"Cache-Control": "max-age=3600",
|
|
27
|
+
"Content-Range": `bytes ${start}-${end}/${stats.size}`,
|
|
28
|
+
"Content-Length": end - start + 1,
|
|
29
|
+
"Accept-Ranges": "bytes"
|
|
30
|
+
});
|
|
31
|
+
log(`Sending ${filePath} range ${start}-${end}/${stats.size}`);
|
|
32
|
+
const readStream = createReadStream(filePath, {
|
|
33
|
+
start,
|
|
34
|
+
end
|
|
35
|
+
});
|
|
36
|
+
readStream.pipe(res);
|
|
37
|
+
} else {
|
|
38
|
+
res.writeHead(200, {
|
|
39
|
+
...headers,
|
|
40
|
+
"Content-Type": mime.getType(filePath) || "text/plain",
|
|
41
|
+
"Cache-Control": "max-age=3600",
|
|
42
|
+
"Content-Length": stats.size
|
|
43
|
+
});
|
|
44
|
+
log(`Sending ${filePath}`);
|
|
45
|
+
const readStream = createReadStream(filePath);
|
|
46
|
+
readStream.pipe(res);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
log("Error sending file", error);
|
|
50
|
+
console.error(error);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export { sendTaskResult };
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/vite-plugin",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.23-beta.0",
|
|
4
4
|
"description": "Editframe vite plugin",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"import": {
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
-
"default": "./dist/
|
|
9
|
+
"default": "./dist/index.js"
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
},
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"author": "",
|
|
20
20
|
"license": "UNLICENSED",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@editframe/assets": "0.18.
|
|
22
|
+
"@editframe/assets": "0.18.23-beta.0",
|
|
23
23
|
"connect": "^3.7.0",
|
|
24
24
|
"debug": "^4.3.5",
|
|
25
25
|
"mime": "^4.0.3",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { idempotentTask } from "../idempotentTask.js";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createReadStream } from "node:fs";
|
|
4
|
-
const cacheImageTask = idempotentTask({
|
|
5
|
-
label: "image",
|
|
6
|
-
filename: (absolutePath) => path.basename(absolutePath),
|
|
7
|
-
runner: async (absolutePath) => {
|
|
8
|
-
return createReadStream(absolutePath);
|
|
9
|
-
}
|
|
10
|
-
});
|
|
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 { cacheImage };
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { idempotentTask } from "../idempotentTask.js";
|
|
2
|
-
import { basename } from "node:path";
|
|
3
|
-
import debug from "debug";
|
|
4
|
-
import { exec } from "node:child_process";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
const execPromise = promisify(exec);
|
|
7
|
-
const log = debug("ef:generateCaptions");
|
|
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;
|
|
13
|
-
};
|
|
14
|
-
const generateCaptionDataTask = idempotentTask({
|
|
15
|
-
label: "captions",
|
|
16
|
-
filename: (absolutePath) => `${basename(absolutePath)}.captions.json`,
|
|
17
|
-
runner: generateCaptionDataFromPath
|
|
18
|
-
});
|
|
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 { findOrCreateCaptions, generateCaptionDataFromPath };
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { idempotentTask } from "../idempotentTask.js";
|
|
2
|
-
import { generateSingleTrackFromPath } from "../generateSingleTrack.js";
|
|
3
|
-
import { basename } from "node:path";
|
|
4
|
-
import debug from "debug";
|
|
5
|
-
const generateTrackFromPath = async (absolutePath, trackId) => {
|
|
6
|
-
const log = debug("ef:generateTrackFragment");
|
|
7
|
-
log(`Generating track ${trackId} for ${absolutePath}`);
|
|
8
|
-
const result = await generateSingleTrackFromPath(absolutePath, trackId);
|
|
9
|
-
return result.stream;
|
|
10
|
-
};
|
|
11
|
-
const generateTrackTask = idempotentTask({
|
|
12
|
-
label: "track",
|
|
13
|
-
filename: (absolutePath, trackId) => `${basename(absolutePath)}.track-${trackId}.mp4`,
|
|
14
|
-
runner: generateTrackFromPath
|
|
15
|
-
});
|
|
16
|
-
const generateTrack = async (cacheRoot, absolutePath, url) => {
|
|
17
|
-
try {
|
|
18
|
-
const trackId = new URL(`http://localhost${url}`).searchParams.get("trackId");
|
|
19
|
-
if (trackId === null) throw new Error("No trackId provided. It must be specified in the query string: ?trackId=1 (for video) or ?trackId=2 (for audio)");
|
|
20
|
-
return await generateTrackTask(cacheRoot, absolutePath, Number(trackId));
|
|
21
|
-
} catch (error) {
|
|
22
|
-
console.error(error);
|
|
23
|
-
console.trace("Error generating track", error);
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
export { generateTrack, generateTrackFromPath };
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Probe } from "../Probe.js";
|
|
2
|
-
import { generateFragmentIndex } from "../generateFragmentIndex.js";
|
|
3
|
-
import { idempotentTask } from "../idempotentTask.js";
|
|
4
|
-
import { basename } from "node:path";
|
|
5
|
-
import debug from "debug";
|
|
6
|
-
const generateTrackFragmentIndexFromPath = async (absolutePath) => {
|
|
7
|
-
const log = debug("ef:generateTrackFragment");
|
|
8
|
-
const probe = await Probe.probePath(absolutePath);
|
|
9
|
-
let startTimeOffsetMs;
|
|
10
|
-
if (probe.format.start_time && Number(probe.format.start_time) !== 0) {
|
|
11
|
-
startTimeOffsetMs = Number(probe.format.start_time) * 1e3;
|
|
12
|
-
log(`Extracted format start_time offset: ${probe.format.start_time}s (${startTimeOffsetMs}ms)`);
|
|
13
|
-
} else {
|
|
14
|
-
const videoStream = probe.streams.find((stream) => stream.codec_type === "video");
|
|
15
|
-
if (videoStream && videoStream.start_time && Number(videoStream.start_time) !== 0) {
|
|
16
|
-
startTimeOffsetMs = Number(videoStream.start_time) * 1e3;
|
|
17
|
-
log(`Extracted video stream start_time offset: ${videoStream.start_time}s (${startTimeOffsetMs}ms)`);
|
|
18
|
-
} else log("No format/stream timing offset found - will detect from composition time");
|
|
19
|
-
}
|
|
20
|
-
log(`Generating track fragment index for ${absolutePath} using single-track approach`);
|
|
21
|
-
const trackFragmentIndexes = {};
|
|
22
|
-
for (let streamIndex = 0; streamIndex < probe.streams.length; streamIndex++) {
|
|
23
|
-
const stream = probe.streams[streamIndex];
|
|
24
|
-
if (stream.codec_type !== "audio" && stream.codec_type !== "video") continue;
|
|
25
|
-
const trackId = streamIndex + 1;
|
|
26
|
-
log(`Processing track ${trackId} (${stream.codec_type})`);
|
|
27
|
-
const trackStream = probe.createTrackReadstream(streamIndex);
|
|
28
|
-
const trackIdMapping = { 0: trackId };
|
|
29
|
-
const singleTrackIndexes = await generateFragmentIndex(trackStream, startTimeOffsetMs, trackIdMapping);
|
|
30
|
-
Object.assign(trackFragmentIndexes, singleTrackIndexes);
|
|
31
|
-
}
|
|
32
|
-
return trackFragmentIndexes;
|
|
33
|
-
};
|
|
34
|
-
const generateTrackFragmentIndexTask = idempotentTask({
|
|
35
|
-
label: "trackFragmentIndex",
|
|
36
|
-
filename: (absolutePath) => `${basename(absolutePath)}.tracks.json`,
|
|
37
|
-
runner: async (absolutePath) => {
|
|
38
|
-
const index = await generateTrackFragmentIndexFromPath(absolutePath);
|
|
39
|
-
return JSON.stringify(index, null, 2);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
const generateTrackFragmentIndex = async (cacheRoot, absolutePath) => {
|
|
43
|
-
try {
|
|
44
|
-
return await generateTrackFragmentIndexTask(cacheRoot, absolutePath);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
console.trace("Error generating track fragment index", error);
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
export { generateTrackFragmentIndex, generateTrackFragmentIndexFromPath };
|