@editframe/vite-plugin 0.38.1 → 0.39.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/index.js
CHANGED
|
@@ -8,7 +8,6 @@ import { cacheImage, findOrCreateCaptions, generateScrubTrack, generateTrack, ge
|
|
|
8
8
|
import debug from "debug";
|
|
9
9
|
|
|
10
10
|
//#region src/index.ts
|
|
11
|
-
const md5ToFilePathMap = /* @__PURE__ */ new Map();
|
|
12
11
|
const getEditframeClient = () => {
|
|
13
12
|
const token = process.env.EF_TOKEN;
|
|
14
13
|
const efHost = process.env.EF_HOST;
|
|
@@ -19,7 +18,10 @@ const vitePluginEditframe = (options) => {
|
|
|
19
18
|
return {
|
|
20
19
|
name: "vite-plugin-editframe",
|
|
21
20
|
configureServer(server) {
|
|
22
|
-
const jitTranscodeMiddleware = createJitTranscodeMiddleware(
|
|
21
|
+
const jitTranscodeMiddleware = createJitTranscodeMiddleware({
|
|
22
|
+
...options,
|
|
23
|
+
handleRemoteUrls: true
|
|
24
|
+
}, {
|
|
23
25
|
generateTrack,
|
|
24
26
|
generateScrubTrack,
|
|
25
27
|
generateTrackFragmentIndex
|
|
@@ -28,7 +30,7 @@ const vitePluginEditframe = (options) => {
|
|
|
28
30
|
server.middlewares.use(async (req, res, next) => {
|
|
29
31
|
const log = debug("ef:vite-plugin");
|
|
30
32
|
const reqUrl = req.url || "";
|
|
31
|
-
if (!reqUrl.startsWith("/api/v1/assets/
|
|
33
|
+
if (!reqUrl.startsWith("/api/v1/assets/")) return next();
|
|
32
34
|
const url = new URL(reqUrl, `http://${req.headers.host}`);
|
|
33
35
|
const urlPath = url.pathname;
|
|
34
36
|
const src = url.searchParams.get("src");
|
|
@@ -37,17 +39,27 @@ const vitePluginEditframe = (options) => {
|
|
|
37
39
|
res.end(JSON.stringify({ error: "src parameter is required" }));
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
+
const isRemote = src.startsWith("http://") || src.startsWith("https://");
|
|
43
|
+
const absolutePath = isRemote ? src : path.join(options.root, src).replace("dist/", "src/");
|
|
44
|
+
log(`Handling assets API: ${urlPath} src=${src}`);
|
|
42
45
|
try {
|
|
43
|
-
if (urlPath === "/api/v1/assets/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
if (urlPath === "/api/v1/assets/image") {
|
|
47
|
+
if (isRemote) {
|
|
48
|
+
const response = await fetch(src);
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
res.writeHead(response.status);
|
|
51
|
+
res.end();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
55
|
+
const buffer = await response.arrayBuffer();
|
|
56
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
57
|
+
res.end(Buffer.from(buffer));
|
|
58
|
+
} else sendTaskResult(req, res, await cacheImage(options.cacheRoot, absolutePath));
|
|
46
59
|
return;
|
|
47
60
|
}
|
|
48
|
-
if (urlPath === "/api/v1/assets/
|
|
49
|
-
|
|
50
|
-
sendTaskResult(req, res, await cacheImage(options.cacheRoot, absolutePath));
|
|
61
|
+
if (urlPath === "/api/v1/assets/captions") {
|
|
62
|
+
sendTaskResult(req, res, await findOrCreateCaptions(options.cacheRoot, absolutePath));
|
|
51
63
|
return;
|
|
52
64
|
}
|
|
53
65
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
@@ -66,24 +78,19 @@ const vitePluginEditframe = (options) => {
|
|
|
66
78
|
server.middlewares.use(async (req, res, next) => {
|
|
67
79
|
const log = debug("ef:vite-plugin");
|
|
68
80
|
const reqUrl = req.url || "";
|
|
69
|
-
if (!reqUrl.startsWith("/api/v1/files/local/") && !reqUrl.startsWith("/api/v1/isobmff_files/local/")) return next();
|
|
70
81
|
const url = new URL(reqUrl, `http://${req.headers.host}`);
|
|
71
82
|
const urlPath = url.pathname;
|
|
72
83
|
const src = url.searchParams.get("src");
|
|
73
|
-
if (!src)
|
|
74
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
75
|
-
res.end(JSON.stringify({ error: "src parameter is required" }));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
84
|
+
if (!src || urlPath !== "/api/v1/files/index" && urlPath !== "/api/v1/files/md5" && urlPath !== "/api/v1/files/track") return next();
|
|
78
85
|
const absolutePath = src.startsWith("http") ? src : path.join(options.root, src).replace("dist/", "src/");
|
|
79
|
-
log(`Handling local
|
|
86
|
+
log(`Handling local file API: ${urlPath} for ${absolutePath}`);
|
|
80
87
|
try {
|
|
81
|
-
if (urlPath === "/api/v1/files/
|
|
88
|
+
if (urlPath === "/api/v1/files/index") {
|
|
82
89
|
log(`Serving track fragment index for ${absolutePath}`);
|
|
83
90
|
sendTaskResult(req, res, await generateTrackFragmentIndex(options.cacheRoot, absolutePath));
|
|
84
91
|
return;
|
|
85
92
|
}
|
|
86
|
-
if (urlPath === "/api/v1/files/
|
|
93
|
+
if (urlPath === "/api/v1/files/md5") {
|
|
87
94
|
log(`Getting MD5 for ${absolutePath}`);
|
|
88
95
|
try {
|
|
89
96
|
const md5 = await md5FilePath(absolutePath);
|
|
@@ -100,7 +107,7 @@ const vitePluginEditframe = (options) => {
|
|
|
100
107
|
}
|
|
101
108
|
return;
|
|
102
109
|
}
|
|
103
|
-
if (urlPath === "/api/v1/files/
|
|
110
|
+
if (urlPath === "/api/v1/files/track") {
|
|
104
111
|
const trackIdStr = url.searchParams.get("trackId");
|
|
105
112
|
const segmentIdStr = url.searchParams.get("segmentId");
|
|
106
113
|
if (!trackIdStr) {
|
|
@@ -119,10 +126,8 @@ const vitePluginEditframe = (options) => {
|
|
|
119
126
|
sendTaskResult(req, res, await generateTrack(options.cacheRoot, absolutePath, trackUrl));
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
123
|
-
res.end(JSON.stringify({ error: "Unknown isobmff endpoint" }));
|
|
124
129
|
} catch (error) {
|
|
125
|
-
log(`Error handling
|
|
130
|
+
log(`Error handling local file request: ${error}`);
|
|
126
131
|
if (error.code === "ENOENT") {
|
|
127
132
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
128
133
|
res.end("File not found");
|
|
@@ -137,8 +142,6 @@ const vitePluginEditframe = (options) => {
|
|
|
137
142
|
if (req.url?.startsWith("/@ef")) forbidRelativePaths(req);
|
|
138
143
|
else return next();
|
|
139
144
|
log(`Handling ${req.url} at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
140
|
-
const assetPath = req.url.replace(/^\/@ef-[^/]+\//, "").replace(/\?.*$/, "");
|
|
141
|
-
const absolutePath = assetPath.startsWith("http") ? assetPath : path.join(options.root, assetPath).replace("dist/", "src/");
|
|
142
145
|
options.cacheRoot = options.cacheRoot.replace("dist/", "src/");
|
|
143
146
|
const efPrefix = req.url.split("/")[1];
|
|
144
147
|
switch (efPrefix) {
|
|
@@ -170,54 +173,6 @@ const vitePluginEditframe = (options) => {
|
|
|
170
173
|
res.end("Cache cleared");
|
|
171
174
|
break;
|
|
172
175
|
}
|
|
173
|
-
case "@ef-asset":
|
|
174
|
-
if (req.method !== "HEAD") {
|
|
175
|
-
res.writeHead(405, { Allow: "HEAD" });
|
|
176
|
-
res.end();
|
|
177
|
-
}
|
|
178
|
-
md5FilePath(absolutePath).then((md5) => {
|
|
179
|
-
md5ToFilePathMap.set(md5, absolutePath);
|
|
180
|
-
log(`Stored MD5 mapping: ${md5} -> ${absolutePath}`);
|
|
181
|
-
res.writeHead(200, { etag: md5 });
|
|
182
|
-
res.end();
|
|
183
|
-
}).catch(next);
|
|
184
|
-
break;
|
|
185
|
-
case "@ef-track-fragment-index": {
|
|
186
|
-
const indexStartTime = Date.now();
|
|
187
|
-
log(`Serving track fragment index for ${absolutePath}`);
|
|
188
|
-
generateTrackFragmentIndex(options.cacheRoot, absolutePath).then((taskResult) => {
|
|
189
|
-
log(`Fragment index generated in ${Date.now() - indexStartTime}ms: ${taskResult.cachePath}`);
|
|
190
|
-
sendTaskResult(req, res, taskResult);
|
|
191
|
-
}).catch((error) => {
|
|
192
|
-
log(`Error generating fragment index after ${Date.now() - indexStartTime}ms:`, error);
|
|
193
|
-
next(error);
|
|
194
|
-
});
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
case "@ef-track": {
|
|
198
|
-
const trackStartTime = Date.now();
|
|
199
|
-
log(`Serving track for ${absolutePath} (cacheRoot: ${options.cacheRoot})`);
|
|
200
|
-
generateTrack(options.cacheRoot, absolutePath, req.url).then((taskResult) => {
|
|
201
|
-
log(`Track generated in ${Date.now() - trackStartTime}ms: ${taskResult.cachePath}`);
|
|
202
|
-
sendTaskResult(req, res, taskResult);
|
|
203
|
-
}).catch((error) => {
|
|
204
|
-
log(`Error generating track after ${Date.now() - trackStartTime}ms:`, error);
|
|
205
|
-
next(error);
|
|
206
|
-
});
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
case "@ef-scrub-track":
|
|
210
|
-
log(`Serving scrub track for ${absolutePath}`);
|
|
211
|
-
generateScrubTrack(options.cacheRoot, absolutePath).then((taskResult) => sendTaskResult(req, res, taskResult)).catch(next);
|
|
212
|
-
break;
|
|
213
|
-
case "@ef-captions":
|
|
214
|
-
log(`Serving captions for ${absolutePath}`);
|
|
215
|
-
findOrCreateCaptions(options.cacheRoot, absolutePath).then((taskResult) => sendTaskResult(req, res, taskResult)).catch(next);
|
|
216
|
-
break;
|
|
217
|
-
case "@ef-image":
|
|
218
|
-
log(`Serving image file ${absolutePath}`);
|
|
219
|
-
cacheImage(options.cacheRoot, absolutePath).then((taskResult) => sendTaskResult(req, res, taskResult)).catch(next);
|
|
220
|
-
break;
|
|
221
176
|
case "@ef-sign-url": {
|
|
222
177
|
if (req.method !== "POST") {
|
|
223
178
|
res.writeHead(405, { Allow: "POST" });
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["error: any"],"sources":["../src/index.ts"],"sourcesContent":["import { rm } from \"node:fs/promises\";\nimport path, { join } from \"node:path\";\nimport { Client, createURLToken } from \"@editframe/api\";\nimport {\n cacheImage,\n findOrCreateCaptions,\n generateTrack,\n generateScrubTrack,\n generateTrackFragmentIndex,\n md5FilePath,\n} from \"@editframe/assets\";\nimport debug from \"debug\";\nimport type { Plugin } from \"vite\";\n\nimport { forbidRelativePaths } from \"./forbidRelativePaths.js\";\nimport { createJitTranscodeMiddleware } from \"./jitTranscodeMiddleware.js\";\nimport { sendTaskResult } from \"./sendTaskResult.js\";\n\ninterface VitePluginEditframeOptions {\n root: string;\n cacheRoot: string;\n}\n\n// In-memory mapping of MD5 -> file path for API route handling\nconst md5ToFilePathMap = new Map<string, string>();\n\n// Create editframe client instance\nconst getEditframeClient = () => {\n const token = process.env.EF_TOKEN;\n const efHost = process.env.EF_HOST;\n if (!token) {\n throw new Error(\"EF_TOKEN environment variable must be set\");\n }\n return new Client(token, efHost);\n};\n\nexport const vitePluginEditframe = (options: VitePluginEditframeOptions) => {\n return {\n name: \"vite-plugin-editframe\",\n\n configureServer(server) {\n // Register JIT transcode middleware first (shared with vitest)\n const jitTranscodeMiddleware = createJitTranscodeMiddleware(options, {\n generateTrack,\n generateScrubTrack,\n generateTrackFragmentIndex,\n });\n server.middlewares.use(jitTranscodeMiddleware);\n\n // Handle local assets API format: /api/v1/assets/local/*\n server.middlewares.use(async (req, res, next) => {\n const log = debug(\"ef:vite-plugin\");\n const reqUrl = req.url || \"\";\n\n if (!reqUrl.startsWith(\"/api/v1/assets/local/\")) {\n return next();\n }\n\n const url = new URL(reqUrl, `http://${req.headers.host}`);\n const urlPath = url.pathname;\n const src = url.searchParams.get(\"src\");\n\n if (!src) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"src parameter is required\" }));\n return;\n }\n\n // Resolve src to absolute file path\n const absolutePath = src.startsWith(\"http\")\n ? src\n : path.join(options.root, src).replace(\"dist/\", \"src/\");\n\n log(`Handling local assets API: ${urlPath} for ${absolutePath}`);\n\n try {\n // Handle /api/v1/assets/local/captions - captions/transcriptions\n if (urlPath === \"/api/v1/assets/local/captions\") {\n log(`Serving captions for ${absolutePath}`);\n const taskResult = await findOrCreateCaptions(\n options.cacheRoot,\n absolutePath,\n );\n sendTaskResult(req, res, taskResult);\n return;\n }\n\n // Handle /api/v1/assets/local/image - cached images\n if (urlPath === \"/api/v1/assets/local/image\") {\n log(`Serving image for ${absolutePath}`);\n const taskResult = await cacheImage(\n options.cacheRoot,\n absolutePath,\n );\n sendTaskResult(req, res, taskResult);\n return;\n }\n\n // Unknown endpoint\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Unknown assets endpoint\" }));\n } catch (error) {\n log(`Error handling assets request: ${error}`);\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: (error as Error).message }));\n }\n }\n });\n\n // Handle production API format for local files: /api/v1/files/local/* and /api/v1/isobmff_files/local/*\n server.middlewares.use(async (req, res, next) => {\n const log = debug(\"ef:vite-plugin\");\n const reqUrl = req.url || \"\";\n\n if (\n !reqUrl.startsWith(\"/api/v1/files/local/\") &&\n !reqUrl.startsWith(\"/api/v1/isobmff_files/local/\")\n ) {\n return next();\n }\n\n const url = new URL(reqUrl, `http://${req.headers.host}`);\n const urlPath = url.pathname;\n const src = url.searchParams.get(\"src\");\n\n if (!src) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"src parameter is required\" }));\n return;\n }\n\n // Resolve src to absolute file path\n const absolutePath = src.startsWith(\"http\")\n ? src\n : path.join(options.root, src).replace(\"dist/\", \"src/\");\n\n log(`Handling local isobmff API: ${urlPath} for ${absolutePath}`);\n\n try {\n // Handle /api/v1/files/local/index or /api/v1/isobmff_files/local/index - fragment index\n if (\n urlPath === \"/api/v1/files/local/index\" ||\n urlPath === \"/api/v1/isobmff_files/local/index\"\n ) {\n log(`Serving track fragment index for ${absolutePath}`);\n const taskResult = await generateTrackFragmentIndex(\n options.cacheRoot,\n absolutePath,\n );\n sendTaskResult(req, res, taskResult);\n return;\n }\n\n // Handle /api/v1/files/local/md5 or /api/v1/isobmff_files/local/md5 - get MD5 hash for a file\n if (\n urlPath === \"/api/v1/files/local/md5\" ||\n urlPath === \"/api/v1/isobmff_files/local/md5\"\n ) {\n log(`Getting MD5 for ${absolutePath}`);\n try {\n const md5 = await md5FilePath(absolutePath);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ md5 }));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: (error as Error).message }));\n }\n }\n return;\n }\n\n // Handle /api/v1/files/local/track or /api/v1/isobmff_files/local/track - track segments\n if (\n urlPath === \"/api/v1/files/local/track\" ||\n urlPath === \"/api/v1/isobmff_files/local/track\"\n ) {\n const trackIdStr = url.searchParams.get(\"trackId\");\n const segmentIdStr = url.searchParams.get(\"segmentId\");\n\n if (!trackIdStr) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({ error: \"trackId parameter is required\" }),\n );\n return;\n }\n\n const trackId = parseInt(trackIdStr, 10);\n\n // For scrub track (trackId -1), use generateScrubTrack\n if (trackId === -1) {\n log(`Serving scrub track for ${absolutePath}`);\n const taskResult = await generateScrubTrack(\n options.cacheRoot,\n absolutePath,\n );\n sendTaskResult(req, res, taskResult);\n return;\n }\n\n // For regular tracks, use generateTrack\n log(\n `Serving track ${trackId} segment ${segmentIdStr || \"all\"} for ${absolutePath}`,\n );\n const trackUrl = `/@ef-track/${src}?trackId=${trackId}${segmentIdStr ? `&segmentId=${segmentIdStr}` : \"\"}`;\n const taskResult = await generateTrack(\n options.cacheRoot,\n absolutePath,\n trackUrl,\n );\n sendTaskResult(req, res, taskResult);\n return;\n }\n\n // Unknown endpoint\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Unknown isobmff endpoint\" }));\n } catch (error) {\n log(`Error handling isobmff request: ${error}`);\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: (error as Error).message }));\n }\n }\n });\n\n // Handle @ef-* format (legacy)\n server.middlewares.use(async (req, res, next) => {\n const log = debug(\"ef:vite-plugin\");\n // Forbid relative paths in any request\n if (req.url?.startsWith(\"/@ef\")) {\n forbidRelativePaths(req);\n } else {\n return next();\n }\n\n log(`Handling ${req.url} at ${new Date().toISOString()}`);\n\n const requestPath = req.url.replace(/^\\/@ef-[^/]+\\//, \"\");\n const assetPath = requestPath.replace(/\\?.*$/, \"\");\n\n const absolutePath = assetPath.startsWith(\"http\")\n ? assetPath\n : path.join(options.root, assetPath).replace(\"dist/\", \"src/\");\n\n options.cacheRoot = options.cacheRoot.replace(\"dist/\", \"src/\");\n\n const efPrefix = req.url.split(\"/\")[1];\n\n switch (efPrefix) {\n case \"@ef-clear-cache\": {\n if (req.method !== \"DELETE\") {\n res.writeHead(405, { Allow: \"DELETE\" });\n res.end();\n break;\n }\n log(`Clearing cache for ${options.cacheRoot}`);\n // Retry cache clearing to handle race conditions with concurrent tests\n const cachePath = join(options.cacheRoot, \".cache\");\n const maxRetries = 3;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n await rm(cachePath, {\n recursive: true,\n force: true,\n });\n break; // Success, exit retry loop\n } catch (error: any) {\n // ENOTEMPTY can happen if files are being written during deletion\n // ENOENT is fine - directory doesn't exist\n if (error.code === \"ENOENT\") {\n break; // Already cleared, done\n }\n if (error.code === \"ENOTEMPTY\" && attempt < maxRetries - 1) {\n // Wait a bit and retry\n await new Promise((resolve) =>\n setTimeout(resolve, 100 * (attempt + 1)),\n );\n continue;\n }\n // Log but don't fail - cache clearing is best-effort\n log(\n `Warning: Cache clear attempt ${attempt + 1} failed: ${error.message}`,\n );\n if (attempt === maxRetries - 1) {\n log(\n `Cache clear failed after ${maxRetries} attempts, continuing anyway`,\n );\n }\n }\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(\"Cache cleared\");\n break;\n }\n case \"@ef-asset\": {\n if (req.method !== \"HEAD\") {\n res.writeHead(405, { Allow: \"HEAD\" });\n res.end();\n }\n md5FilePath(absolutePath)\n .then((md5) => {\n // Store mapping for API route handling\n md5ToFilePathMap.set(md5, absolutePath);\n log(`Stored MD5 mapping: ${md5} -> ${absolutePath}`);\n res.writeHead(200, {\n etag: md5,\n });\n res.end();\n })\n .catch(next);\n break;\n }\n case \"@ef-track-fragment-index\": {\n const indexStartTime = Date.now();\n log(`Serving track fragment index for ${absolutePath}`);\n generateTrackFragmentIndex(options.cacheRoot, absolutePath)\n .then((taskResult) => {\n const elapsed = Date.now() - indexStartTime;\n log(\n `Fragment index generated in ${elapsed}ms: ${taskResult.cachePath}`,\n );\n sendTaskResult(req, res, taskResult);\n })\n .catch((error) => {\n const elapsed = Date.now() - indexStartTime;\n log(\n `Error generating fragment index after ${elapsed}ms:`,\n error,\n );\n next(error);\n });\n break;\n }\n case \"@ef-track\": {\n const trackStartTime = Date.now();\n log(\n `Serving track for ${absolutePath} (cacheRoot: ${options.cacheRoot})`,\n );\n generateTrack(options.cacheRoot, absolutePath, req.url)\n .then((taskResult) => {\n const elapsed = Date.now() - trackStartTime;\n log(`Track generated in ${elapsed}ms: ${taskResult.cachePath}`);\n sendTaskResult(req, res, taskResult);\n })\n .catch((error) => {\n const elapsed = Date.now() - trackStartTime;\n log(`Error generating track after ${elapsed}ms:`, error);\n next(error);\n });\n break;\n }\n case \"@ef-scrub-track\": {\n log(`Serving scrub track for ${absolutePath}`);\n generateScrubTrack(options.cacheRoot, absolutePath)\n .then((taskResult) => sendTaskResult(req, res, taskResult))\n .catch(next);\n break;\n }\n case \"@ef-captions\":\n log(`Serving captions for ${absolutePath}`);\n findOrCreateCaptions(options.cacheRoot, absolutePath)\n .then((taskResult) => sendTaskResult(req, res, taskResult))\n .catch(next);\n break;\n case \"@ef-image\":\n log(`Serving image file ${absolutePath}`);\n cacheImage(options.cacheRoot, absolutePath)\n .then((taskResult) => sendTaskResult(req, res, taskResult))\n .catch(next);\n break;\n case \"@ef-sign-url\": {\n if (req.method !== \"POST\") {\n res.writeHead(405, { Allow: \"POST\" });\n res.end();\n break;\n }\n\n log(\"Signing URL token\");\n\n // Parse request body\n let body = \"\";\n req.on(\"data\", (chunk) => {\n body += chunk.toString();\n });\n\n req.on(\"end\", async () => {\n try {\n const payload = JSON.parse(body);\n log(\"Token signing request payload:\", payload);\n\n // Handle both formats: { url } and { url, params }\n const { url, params } = payload;\n if (!url) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"URL is required\" }));\n return;\n }\n\n const client = getEditframeClient();\n\n // createURLToken expects just a string URL\n // For transcode URLs with params, we need to reconstruct the full URL\n let fullUrl = url;\n if (params) {\n const urlObj = new URL(url);\n // Add params as query parameters\n Object.entries(params).forEach(([key, value]) => {\n urlObj.searchParams.set(key, String(value));\n });\n fullUrl = urlObj.toString();\n }\n\n log(\"Creating token for full URL:\", fullUrl);\n const token = await createURLToken(client, fullUrl);\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ token }));\n } catch (error) {\n log(`Error signing URL token: ${error}`);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Failed to sign URL token\" }));\n }\n });\n\n break;\n }\n default:\n log(`Unknown asset type ${efPrefix}`);\n break;\n }\n });\n },\n } satisfies Plugin;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["error: any"],"sources":["../src/index.ts"],"sourcesContent":["import { rm } from \"node:fs/promises\";\nimport path, { join } from \"node:path\";\nimport { Client, createURLToken } from \"@editframe/api\";\nimport {\n cacheImage,\n findOrCreateCaptions,\n generateTrack,\n generateScrubTrack,\n generateTrackFragmentIndex,\n md5FilePath,\n} from \"@editframe/assets\";\nimport debug from \"debug\";\nimport type { Plugin } from \"vite\";\n\nimport { forbidRelativePaths } from \"./forbidRelativePaths.js\";\nimport { createJitTranscodeMiddleware } from \"./jitTranscodeMiddleware.js\";\nimport { sendTaskResult } from \"./sendTaskResult.js\";\n\ninterface VitePluginEditframeOptions {\n root: string;\n cacheRoot: string;\n}\n\n// Create editframe client instance\nconst getEditframeClient = () => {\n const token = process.env.EF_TOKEN;\n const efHost = process.env.EF_HOST;\n if (!token) {\n throw new Error(\"EF_TOKEN environment variable must be set\");\n }\n return new Client(token, efHost);\n};\n\nexport const vitePluginEditframe = (options: VitePluginEditframeOptions) => {\n return {\n name: \"vite-plugin-editframe\",\n\n configureServer(server) {\n // Register JIT transcode middleware first (shared with vitest)\n const jitTranscodeMiddleware = createJitTranscodeMiddleware(\n { ...options, handleRemoteUrls: true },\n {\n generateTrack,\n generateScrubTrack,\n generateTrackFragmentIndex,\n },\n );\n server.middlewares.use(jitTranscodeMiddleware);\n\n // Handle assets API: /api/v1/assets/image and /api/v1/assets/captions\n // src= accepts both local file paths and remote http/https URLs.\n // Remote URLs are fetched server-side so the browser receives them as same-origin,\n // preventing canvas CORS taint when drawImage() is called during rendering.\n server.middlewares.use(async (req, res, next) => {\n const log = debug(\"ef:vite-plugin\");\n const reqUrl = req.url || \"\";\n\n if (!reqUrl.startsWith(\"/api/v1/assets/\")) {\n return next();\n }\n\n const url = new URL(reqUrl, `http://${req.headers.host}`);\n const urlPath = url.pathname;\n const src = url.searchParams.get(\"src\");\n\n if (!src) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"src parameter is required\" }));\n return;\n }\n\n const isRemote =\n src.startsWith(\"http://\") || src.startsWith(\"https://\");\n const absolutePath = isRemote\n ? src\n : path.join(options.root, src).replace(\"dist/\", \"src/\");\n\n log(`Handling assets API: ${urlPath} src=${src}`);\n\n try {\n if (urlPath === \"/api/v1/assets/image\") {\n if (isRemote) {\n const response = await fetch(src);\n if (!response.ok) {\n res.writeHead(response.status);\n res.end();\n return;\n }\n const contentType =\n response.headers.get(\"content-type\") ??\n \"application/octet-stream\";\n const buffer = await response.arrayBuffer();\n res.writeHead(200, { \"Content-Type\": contentType });\n res.end(Buffer.from(buffer));\n } else {\n const taskResult = await cacheImage(\n options.cacheRoot,\n absolutePath,\n );\n sendTaskResult(req, res, taskResult);\n }\n return;\n }\n\n if (urlPath === \"/api/v1/assets/captions\") {\n const taskResult = await findOrCreateCaptions(\n options.cacheRoot,\n absolutePath,\n );\n sendTaskResult(req, res, taskResult);\n return;\n }\n\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Unknown assets endpoint\" }));\n } catch (error) {\n log(`Error handling assets request: ${error}`);\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: (error as Error).message }));\n }\n }\n });\n\n // Handle local file API: /api/v1/files/index, /api/v1/files/md5, /api/v1/files/track (src= identifies local source)\n server.middlewares.use(async (req, res, next) => {\n const log = debug(\"ef:vite-plugin\");\n const reqUrl = req.url || \"\";\n\n const url = new URL(reqUrl, `http://${req.headers.host}`);\n const urlPath = url.pathname;\n const src = url.searchParams.get(\"src\");\n\n if (\n !src ||\n (urlPath !== \"/api/v1/files/index\" &&\n urlPath !== \"/api/v1/files/md5\" &&\n urlPath !== \"/api/v1/files/track\")\n ) {\n return next();\n }\n\n // Resolve src to absolute file path\n const absolutePath = src.startsWith(\"http\")\n ? src\n : path.join(options.root, src).replace(\"dist/\", \"src/\");\n\n log(`Handling local file API: ${urlPath} for ${absolutePath}`);\n\n try {\n if (urlPath === \"/api/v1/files/index\") {\n log(`Serving track fragment index for ${absolutePath}`);\n const taskResult = await generateTrackFragmentIndex(\n options.cacheRoot,\n absolutePath,\n );\n sendTaskResult(req, res, taskResult);\n return;\n }\n\n if (urlPath === \"/api/v1/files/md5\") {\n log(`Getting MD5 for ${absolutePath}`);\n try {\n const md5 = await md5FilePath(absolutePath);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ md5 }));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: (error as Error).message }));\n }\n }\n return;\n }\n\n if (urlPath === \"/api/v1/files/track\") {\n const trackIdStr = url.searchParams.get(\"trackId\");\n const segmentIdStr = url.searchParams.get(\"segmentId\");\n\n if (!trackIdStr) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({ error: \"trackId parameter is required\" }),\n );\n return;\n }\n\n const trackId = parseInt(trackIdStr, 10);\n\n if (trackId === -1) {\n log(`Serving scrub track for ${absolutePath}`);\n const taskResult = await generateScrubTrack(\n options.cacheRoot,\n absolutePath,\n );\n sendTaskResult(req, res, taskResult);\n return;\n }\n\n log(\n `Serving track ${trackId} segment ${segmentIdStr || \"all\"} for ${absolutePath}`,\n );\n const trackUrl = `/@ef-track/${src}?trackId=${trackId}${segmentIdStr ? `&segmentId=${segmentIdStr}` : \"\"}`;\n const taskResult = await generateTrack(\n options.cacheRoot,\n absolutePath,\n trackUrl,\n );\n sendTaskResult(req, res, taskResult);\n return;\n }\n } catch (error) {\n log(`Error handling local file request: ${error}`);\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: (error as Error).message }));\n }\n }\n });\n\n // Handle @ef-* format (legacy)\n server.middlewares.use(async (req, res, next) => {\n const log = debug(\"ef:vite-plugin\");\n // Forbid relative paths in any request\n if (req.url?.startsWith(\"/@ef\")) {\n forbidRelativePaths(req);\n } else {\n return next();\n }\n\n log(`Handling ${req.url} at ${new Date().toISOString()}`);\n\n options.cacheRoot = options.cacheRoot.replace(\"dist/\", \"src/\");\n\n const efPrefix = req.url.split(\"/\")[1];\n\n switch (efPrefix) {\n case \"@ef-clear-cache\": {\n if (req.method !== \"DELETE\") {\n res.writeHead(405, { Allow: \"DELETE\" });\n res.end();\n break;\n }\n log(`Clearing cache for ${options.cacheRoot}`);\n // Retry cache clearing to handle race conditions with concurrent tests\n const cachePath = join(options.cacheRoot, \".cache\");\n const maxRetries = 3;\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n await rm(cachePath, {\n recursive: true,\n force: true,\n });\n break; // Success, exit retry loop\n } catch (error: any) {\n // ENOTEMPTY can happen if files are being written during deletion\n // ENOENT is fine - directory doesn't exist\n if (error.code === \"ENOENT\") {\n break; // Already cleared, done\n }\n if (error.code === \"ENOTEMPTY\" && attempt < maxRetries - 1) {\n // Wait a bit and retry\n await new Promise((resolve) =>\n setTimeout(resolve, 100 * (attempt + 1)),\n );\n continue;\n }\n // Log but don't fail - cache clearing is best-effort\n log(\n `Warning: Cache clear attempt ${attempt + 1} failed: ${error.message}`,\n );\n if (attempt === maxRetries - 1) {\n log(\n `Cache clear failed after ${maxRetries} attempts, continuing anyway`,\n );\n }\n }\n }\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(\"Cache cleared\");\n break;\n }\n case \"@ef-sign-url\": {\n if (req.method !== \"POST\") {\n res.writeHead(405, { Allow: \"POST\" });\n res.end();\n break;\n }\n\n log(\"Signing URL token\");\n\n // Parse request body\n let body = \"\";\n req.on(\"data\", (chunk) => {\n body += chunk.toString();\n });\n\n req.on(\"end\", async () => {\n try {\n const payload = JSON.parse(body);\n log(\"Token signing request payload:\", payload);\n\n // Handle both formats: { url } and { url, params }\n const { url, params } = payload;\n if (!url) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"URL is required\" }));\n return;\n }\n\n const client = getEditframeClient();\n\n // createURLToken expects just a string URL\n // For transcode URLs with params, we need to reconstruct the full URL\n let fullUrl = url;\n if (params) {\n const urlObj = new URL(url);\n // Add params as query parameters\n Object.entries(params).forEach(([key, value]) => {\n urlObj.searchParams.set(key, String(value));\n });\n fullUrl = urlObj.toString();\n }\n\n log(\"Creating token for full URL:\", fullUrl);\n const token = await createURLToken(client, fullUrl);\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ token }));\n } catch (error) {\n log(`Error signing URL token: ${error}`);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Failed to sign URL token\" }));\n }\n });\n\n break;\n }\n default:\n log(`Unknown asset type ${efPrefix}`);\n break;\n }\n });\n },\n } satisfies Plugin;\n};\n"],"mappings":"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"}
|
|
@@ -202,8 +202,8 @@ async function generateLocalJitManifest(absolutePath, sourceUrl, baseUrl, cacheR
|
|
|
202
202
|
language: "en"
|
|
203
203
|
}] : [],
|
|
204
204
|
endpoints: {
|
|
205
|
-
initSegment: `${baseUrl}/api/v1/transcode/{rendition}/init.
|
|
206
|
-
mediaSegment: `${baseUrl}/api/v1/transcode/{rendition}/{segmentId}.
|
|
205
|
+
initSegment: `${baseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,
|
|
206
|
+
mediaSegment: `${baseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`
|
|
207
207
|
},
|
|
208
208
|
jitInfo: {
|
|
209
209
|
parallelTranscodingSupported: true,
|
|
@@ -234,7 +234,7 @@ function createJitTranscodeMiddleware(options, assetFunctions) {
|
|
|
234
234
|
return;
|
|
235
235
|
}
|
|
236
236
|
const mediaPath = resolveMediaPath(sourceUrl, options.root);
|
|
237
|
-
if (mediaPath.startsWith("http://") || mediaPath.startsWith("https://")) return next();
|
|
237
|
+
if (!options.handleRemoteUrls && (mediaPath.startsWith("http://") || mediaPath.startsWith("https://"))) return next();
|
|
238
238
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
239
239
|
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
240
240
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
@@ -253,7 +253,7 @@ function createJitTranscodeMiddleware(options, assetFunctions) {
|
|
|
253
253
|
const [, rendition, endpoint] = pathMatch;
|
|
254
254
|
if (endpoint === "manifest.json") {
|
|
255
255
|
log(`Generating manifest for ${mediaPath}`);
|
|
256
|
-
const baseUrl = `${url.protocol}
|
|
256
|
+
const baseUrl = `${req.headers["x-forwarded-proto"] || url.protocol.replace(":", "")}://${url.host}`;
|
|
257
257
|
try {
|
|
258
258
|
const manifest = await generateLocalJitManifest(mediaPath, sourceUrl, baseUrl, options.cacheRoot, assetFunctions);
|
|
259
259
|
res.writeHead(200, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jitTranscodeMiddleware.js","names":["fragmentIndex: Record<number, TrackFragmentIndex>"],"sources":["../src/jitTranscodeMiddleware.ts"],"sourcesContent":["import { createReadStream, statSync } from \"node:fs\";\nimport type { ServerResponse } from \"node:http\";\nimport path from \"node:path\";\n\nimport type { IncomingMessage, NextFunction } from \"connect\";\nimport debug from \"debug\";\nimport mime from \"mime\";\n\nimport { forbidRelativePaths } from \"./forbidRelativePaths.js\";\n\n/**\n * Type for the TrackFragmentIndex from assets package.\n * Duplicated here to avoid import issues between package and direct imports.\n */\nexport interface TrackFragmentIndex {\n type: \"video\" | \"audio\";\n codec: string;\n duration: number;\n timescale: number;\n startTimeOffsetMs: number;\n initSegment: { offset: number; size: number };\n segments: Array<{ offset: number; size: number; duration: number }>;\n width?: number;\n height?: number;\n channel_count?: number;\n sample_rate?: number;\n}\n\n/**\n * Asset functions required by the JIT middleware.\n * These are injected to support both package imports and direct imports.\n */\nexport interface AssetFunctions {\n generateTrack: (\n cacheRoot: string,\n absolutePath: string,\n trackUrl: string,\n ) => Promise<{ cachePath: string }>;\n generateScrubTrack: (\n cacheRoot: string,\n absolutePath: string,\n ) => Promise<{ cachePath: string }>;\n generateTrackFragmentIndex: (\n cacheRoot: string,\n absolutePath: string,\n ) => Promise<{ cachePath: string }>;\n}\n\nexport interface JitMiddlewareOptions {\n root: string;\n cacheRoot: string;\n}\n\n/**\n * Stream a specific byte range from a file.\n * This is used for JIT segment serving where the server extracts the correct bytes.\n */\nexport function sendByteRange(\n res: ServerResponse,\n filePath: string,\n offset: number,\n size: number,\n contentType?: string,\n) {\n const log = debug(\"ef:sendByteRange\");\n const stats = statSync(filePath);\n const end = offset + size - 1;\n\n if (end >= stats.size) {\n log(`Requested range ${offset}-${end} exceeds file size ${stats.size}`);\n res.writeHead(416, { \"Content-Range\": `bytes */${stats.size}` });\n res.end();\n return;\n }\n\n log(`Streaming bytes ${offset}-${end} (${size} bytes) from ${filePath}`);\n\n res.writeHead(200, {\n \"Content-Type\": contentType || mime.getType(filePath) || \"video/mp4\",\n \"Content-Length\": size,\n \"Cache-Control\": \"public, max-age=3600\",\n });\n\n const readStream = createReadStream(filePath, { start: offset, end });\n readStream.pipe(res);\n}\n\n/**\n * Stream multiple byte ranges from a file concatenated together.\n * Used for creating playable .mp4 files by combining init segment + media segment.\n */\nexport function sendMultipleByteRanges(\n res: ServerResponse,\n filePath: string,\n ranges: Array<{ offset: number; size: number }>,\n contentType?: string,\n) {\n const log = debug(\"ef:sendMultipleByteRanges\");\n const stats = statSync(filePath);\n\n // Validate all ranges\n for (const range of ranges) {\n const end = range.offset + range.size - 1;\n if (end >= stats.size) {\n log(\n `Requested range ${range.offset}-${end} exceeds file size ${stats.size}`,\n );\n res.writeHead(416, { \"Content-Range\": `bytes */${stats.size}` });\n res.end();\n return;\n }\n }\n\n const totalSize = ranges.reduce((sum, r) => sum + r.size, 0);\n log(\n `Streaming ${ranges.length} ranges (${totalSize} total bytes) from ${filePath}`,\n );\n\n res.writeHead(200, {\n \"Content-Type\": contentType || \"video/mp4\",\n \"Content-Length\": totalSize,\n \"Cache-Control\": \"public, max-age=3600\",\n });\n\n // Stream ranges sequentially\n let rangeIndex = 0;\n\n const streamNextRange = () => {\n if (rangeIndex >= ranges.length) {\n res.end();\n return;\n }\n\n const range = ranges[rangeIndex]!;\n const end = range.offset + range.size - 1;\n const readStream = createReadStream(filePath, { start: range.offset, end });\n\n readStream.on(\"end\", () => {\n rangeIndex++;\n streamNextRange();\n });\n\n readStream.on(\"error\", (err) => {\n log(`Error streaming range ${rangeIndex}: ${err}`);\n res.destroy();\n });\n\n readStream.pipe(res, { end: false });\n };\n\n streamNextRange();\n}\n\n/**\n * Check if a hostname refers to the local vite server.\n * Handles various local hostname patterns including worktree domains.\n */\nexport function isLocalHost(hostname: string): boolean {\n const localPatterns = [\n \"localhost\",\n \"127.0.0.1\",\n \"0.0.0.0\",\n \".localhost\", // Matches *.localhost (worktree domains like main.localhost)\n ];\n\n const lowerHost = hostname.toLowerCase();\n return localPatterns.some((pattern) =>\n pattern.startsWith(\".\")\n ? lowerHost.endsWith(pattern) || lowerHost === pattern.slice(1)\n : lowerHost === pattern || lowerHost.startsWith(pattern + \":\"),\n );\n}\n\n/**\n * Resolve a URL to either a remote URL (for ffprobe) or a local file path.\n *\n * - Remote URLs (different host): passed directly to ffprobe (it supports http/https)\n * - Local URLs (localhost, *.localhost): resolved to local file path\n *\n * @param urlParam - The URL from the query parameter\n * @param root - The vite plugin root directory\n * @returns The path/URL to pass to ffprobe\n */\nexport function resolveMediaPath(urlParam: string, root: string): string {\n try {\n const url = new URL(urlParam);\n const hostname = url.hostname;\n\n // If NOT a local URL, pass directly to ffprobe - it supports http/https URLs\n if (!isLocalHost(hostname)) {\n return urlParam;\n }\n\n // Local URL - resolve to file path\n let filePath = decodeURIComponent(url.pathname);\n\n // Remove leading slash for path.join\n if (filePath.startsWith(\"/\")) {\n filePath = filePath.slice(1);\n }\n\n // The root is already dev-projects/src, so if the URL path starts with \"src/\",\n // we should remove it to avoid duplication (src/src/assets -> src/assets)\n if (filePath.startsWith(\"src/\")) {\n filePath = filePath.slice(4); // Remove \"src/\"\n }\n\n return path.join(root, filePath);\n } catch {\n // If not a valid URL, treat as relative path\n let filePath = urlParam;\n if (filePath.startsWith(\"src/\")) {\n filePath = filePath.slice(4);\n }\n return path.join(root, filePath);\n }\n}\n\n/**\n * Convert fragment index segments to millisecond durations array.\n */\nexport function getSegmentDurationsMs(track: TrackFragmentIndex): number[] {\n return track.segments.map(\n (segment) => (segment.duration / track.timescale) * 1000,\n );\n}\n\n/**\n * Generate a JIT manifest for a local file.\n * Uses actual fragment index data for accurate segment information.\n */\nexport async function generateLocalJitManifest(\n absolutePath: string,\n sourceUrl: string,\n baseUrl: string,\n cacheRoot: string,\n assetFunctions: AssetFunctions,\n) {\n const log = debug(\"ef:generateLocalJitManifest\");\n\n // Generate the fragment index (this also ensures tracks are generated)\n log(`Generating fragment index for ${absolutePath}`);\n const fragmentIndexResult = await assetFunctions.generateTrackFragmentIndex(\n cacheRoot,\n absolutePath,\n );\n const fragmentIndex: Record<number, TrackFragmentIndex> = JSON.parse(\n await import(\"node:fs/promises\").then((fs) =>\n fs.readFile(fragmentIndexResult.cachePath, \"utf-8\"),\n ),\n );\n\n // Find video track (track 1) and audio track (track 2) and scrub track (-1)\n const videoTrack = fragmentIndex[1];\n const audioTrack = fragmentIndex[2];\n const scrubTrack = fragmentIndex[-1];\n\n const hasVideo = videoTrack?.type === \"video\";\n const hasAudio = audioTrack?.type === \"audio\";\n\n // Get duration from the longest track\n let durationMs = 0;\n if (hasVideo && videoTrack) {\n durationMs = Math.max(\n durationMs,\n (videoTrack.duration / videoTrack.timescale) * 1000,\n );\n }\n if (hasAudio && audioTrack) {\n durationMs = Math.max(\n durationMs,\n (audioTrack.duration / audioTrack.timescale) * 1000,\n );\n }\n const durationSeconds = durationMs / 1000;\n\n // Get video dimensions from track\n const width =\n hasVideo && videoTrack && \"width\" in videoTrack ? videoTrack.width : 1920;\n const height =\n hasVideo && videoTrack && \"height\" in videoTrack ? videoTrack.height : 1080;\n const codec = hasVideo && videoTrack ? videoTrack.codec : \"avc1.640029\";\n\n // Get actual segment durations from fragment index\n const videoSegmentDurationsMs =\n hasVideo && videoTrack ? getSegmentDurationsMs(videoTrack) : [];\n const scrubSegmentDurationsMs = scrubTrack\n ? getSegmentDurationsMs(scrubTrack)\n : [];\n const audioSegmentDurationsMs =\n hasAudio && audioTrack ? getSegmentDurationsMs(audioTrack) : [];\n\n // Average segment duration for backward compatibility\n const avgVideoSegmentDurationMs =\n videoSegmentDurationsMs.length > 0\n ? videoSegmentDurationsMs.reduce((a, b) => a + b, 0) /\n videoSegmentDurationsMs.length\n : 2000;\n const avgScrubSegmentDurationMs =\n scrubSegmentDurationsMs.length > 0\n ? scrubSegmentDurationsMs.reduce((a, b) => a + b, 0) /\n scrubSegmentDurationsMs.length\n : 30000;\n const avgAudioSegmentDurationMs =\n audioSegmentDurationsMs.length > 0\n ? audioSegmentDurationsMs.reduce((a, b) => a + b, 0) /\n audioSegmentDurationsMs.length\n : 2000;\n\n log(\n `Video: ${videoSegmentDurationsMs.length} segments, Audio: ${audioSegmentDurationsMs.length} segments, Scrub: ${scrubSegmentDurationsMs.length} segments`,\n );\n\n // Construct manifest matching ManifestResponse format\n const manifest = {\n version: \"1.0\",\n type: \"com.editframe/local-jit-manifest\",\n sourceUrl: sourceUrl,\n duration: durationSeconds,\n durationMs: durationMs,\n baseUrl: baseUrl,\n\n videoRenditions: hasVideo\n ? [\n {\n id: \"high\",\n width: width,\n height: height,\n bitrate: 5000000,\n codec: codec,\n container: \"video/mp4\",\n mimeType: `video/mp4; codecs=\"${codec}\"`,\n segmentDuration: avgVideoSegmentDurationMs / 1000,\n segmentDurationMs: avgVideoSegmentDurationMs,\n segmentDurationsMs: videoSegmentDurationsMs,\n startTimeOffsetMs: videoTrack!.startTimeOffsetMs,\n frameRate: 30,\n profile: \"High\",\n level: \"4.1\",\n },\n ...(scrubTrack\n ? [\n {\n id: \"scrub\",\n width: 320,\n height: Math.round(\n (320 * (height ?? 1080)) / (width ?? 1920),\n ),\n bitrate: 100000,\n codec: scrubTrack.codec,\n container: \"video/mp4\",\n mimeType: `video/mp4; codecs=\"${scrubTrack.codec}\"`,\n segmentDuration: avgScrubSegmentDurationMs / 1000,\n segmentDurationMs: avgScrubSegmentDurationMs,\n segmentDurationsMs: scrubSegmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n frameRate: 15,\n profile: \"High\",\n level: \"4.1\",\n },\n ]\n : []),\n ]\n : [],\n\n audioRenditions:\n hasAudio && audioTrack\n ? [\n {\n id: \"audio\",\n channels:\n \"channel_count\" in audioTrack ? audioTrack.channel_count : 2,\n sampleRate:\n \"sample_rate\" in audioTrack ? audioTrack.sample_rate : 48000,\n bitrate: 128000,\n codec: audioTrack.codec,\n container: \"audio/mp4\",\n mimeType: `audio/mp4; codecs=\"${audioTrack.codec}\"`,\n segmentDuration: avgAudioSegmentDurationMs / 1000,\n segmentDurationMs: avgAudioSegmentDurationMs,\n segmentDurationsMs: audioSegmentDurationsMs,\n language: \"en\",\n },\n ]\n : [],\n\n endpoints: {\n initSegment: `${baseUrl}/api/v1/transcode/{rendition}/init.mp4?url=${encodeURIComponent(sourceUrl)}`,\n mediaSegment: `${baseUrl}/api/v1/transcode/{rendition}/{segmentId}.mp4?url=${encodeURIComponent(sourceUrl)}`,\n },\n\n jitInfo: {\n parallelTranscodingSupported: true,\n expectedTranscodeLatency: 100, // Local is fast\n segmentCount: videoSegmentDurationsMs.length,\n scrubSegmentCount: scrubSegmentDurationsMs.length,\n },\n };\n\n return manifest;\n}\n\n/**\n * Create the JIT transcode middleware for /api/v1/transcode/* routes.\n *\n * @param options - The middleware options (root, cacheRoot)\n * @param assetFunctions - The asset functions to use (allows dependency injection)\n * @returns Express-compatible middleware function\n */\nexport function createJitTranscodeMiddleware(\n options: JitMiddlewareOptions,\n assetFunctions: AssetFunctions,\n) {\n return async (\n req: IncomingMessage,\n res: ServerResponse,\n next: NextFunction,\n ) => {\n const log = debug(\"ef:vite-plugin:jit\");\n\n if (!req.url?.startsWith(\"/api/v1/transcode/\")) {\n return next();\n }\n\n forbidRelativePaths(req);\n log(`Handling JIT transcode request: ${req.url}`);\n\n const url = new URL(req.url, `http://${req.headers.host}`);\n const sourceUrl = url.searchParams.get(\"url\");\n\n if (!sourceUrl) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"url parameter is required\" }));\n return;\n }\n\n // Resolve URL to either local file path or keep as remote URL (ffprobe supports both)\n const mediaPath = resolveMediaPath(sourceUrl, options.root);\n\n // If the source is a remote URL (not a local file), let the next middleware handle it.\n // This allows recordReplayProxyPlugin to serve cached remote-URL responses.\n if (mediaPath.startsWith(\"http://\") || mediaPath.startsWith(\"https://\")) {\n return next();\n }\n\n // Handle CORS\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization\",\n );\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n try {\n // Parse the path to determine which endpoint\n const pathMatch = url.pathname.match(\n /^\\/api\\/v1\\/transcode\\/(?:([^/]+)\\/)?(.+)$/,\n );\n\n if (!pathMatch) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid transcode endpoint\" }));\n return;\n }\n\n const [, rendition, endpoint] = pathMatch;\n\n // Handle manifest.json endpoint\n if (endpoint === \"manifest.json\") {\n log(`Generating manifest for ${mediaPath}`);\n const baseUrl = `${url.protocol}//${url.host}`;\n\n try {\n const manifest = await generateLocalJitManifest(\n mediaPath,\n sourceUrl,\n baseUrl,\n options.cacheRoot,\n assetFunctions,\n );\n\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"public, max-age=300\",\n });\n res.end(JSON.stringify(manifest, null, 2));\n } catch (error) {\n log(`Error generating manifest: ${error}`);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Failed to generate manifest\",\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n return;\n }\n\n // Get fragment index first - we need it to determine track IDs\n const fragmentIndexResult =\n await assetFunctions.generateTrackFragmentIndex(\n options.cacheRoot,\n mediaPath,\n );\n const fragmentIndex: Record<number, TrackFragmentIndex> = JSON.parse(\n await import(\"node:fs/promises\").then((fs) =>\n fs.readFile(fragmentIndexResult.cachePath, \"utf-8\"),\n ),\n );\n\n // Helper: Map rendition ID to track ID, using fragment index to find correct track\n // For video files: track 1 = video, track 2 = audio\n // For audio-only files: track 1 = audio (no track 2)\n const getTrackId = (renditionId: string): number => {\n if (renditionId === \"scrub\") return -1;\n\n if (renditionId === \"audio\") {\n // Find the audio track - could be track 1 (audio-only) or track 2 (video+audio)\n for (const [trackIdStr, trackInfo] of Object.entries(fragmentIndex)) {\n if (trackInfo.type === \"audio\") {\n return Number.parseInt(trackIdStr, 10);\n }\n }\n // Fallback to track 2 if no audio track found by type\n return 2;\n }\n\n // For video renditions (high, medium, low), find the video track\n for (const [trackIdStr, trackInfo] of Object.entries(fragmentIndex)) {\n if (trackInfo.type === \"video\") {\n return Number.parseInt(trackIdStr, 10);\n }\n }\n // Fallback to track 1\n return 1;\n };\n\n // Handle init segment endpoint (e.g., /api/v1/transcode/high/init.mp4 or init.m4s)\n const initMatch = endpoint?.match(/^init\\.(mp4|m4s)$/);\n if (initMatch && rendition) {\n const extension = initMatch[1];\n const contentType =\n extension === \"m4s\" ? \"video/iso.segment\" : \"video/mp4\";\n log(\n `Serving init segment (${extension}) for ${mediaPath}, rendition: ${rendition}`,\n );\n\n try {\n const trackId = getTrackId(rendition);\n\n // Generate/get the track file\n let trackTaskResult;\n if (trackId === -1) {\n trackTaskResult = await assetFunctions.generateScrubTrack(\n options.cacheRoot,\n mediaPath,\n );\n } else {\n const trackUrl = `/@ef-track/${mediaPath}?trackId=${trackId}`;\n trackTaskResult = await assetFunctions.generateTrack(\n options.cacheRoot,\n mediaPath,\n trackUrl,\n );\n }\n\n const track = fragmentIndex[trackId];\n if (!track) {\n const validTracks = Object.keys(fragmentIndex).join(\", \");\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: `Track ${trackId} not found (valid tracks: ${validTracks})`,\n }),\n );\n return;\n }\n\n // Stream only the init segment bytes\n const { offset, size } = track.initSegment;\n log(`Init segment: offset=${offset}, size=${size}`);\n sendByteRange(\n res,\n trackTaskResult.cachePath,\n offset,\n size,\n contentType,\n );\n } catch (error) {\n log(`Error serving init segment: ${error}`);\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Failed to generate init segment\",\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n }\n return;\n }\n\n // Handle media segment endpoint\n // - .m4s: moof+mdat only (fragment for streaming)\n // - .mp4: init+moof+mdat (playable standalone file for testing)\n const segmentMatch = endpoint?.match(/^(\\d+)\\.(mp4|m4s)$/);\n if (segmentMatch?.[1] && segmentMatch?.[2] && rendition) {\n const segmentId = Number.parseInt(segmentMatch[1], 10);\n const extension = segmentMatch[2];\n const includeInit = extension === \"mp4\";\n log(\n `Serving media segment ${segmentId}.${extension} for ${mediaPath}, rendition: ${rendition}, includeInit: ${includeInit}`,\n );\n\n try {\n const trackId = getTrackId(rendition);\n\n // Generate/get the track file\n let trackTaskResult;\n if (trackId === -1) {\n trackTaskResult = await assetFunctions.generateScrubTrack(\n options.cacheRoot,\n mediaPath,\n );\n } else {\n const trackUrl = `/@ef-track/${mediaPath}?trackId=${trackId}`;\n trackTaskResult = await assetFunctions.generateTrack(\n options.cacheRoot,\n mediaPath,\n trackUrl,\n );\n }\n\n const track = fragmentIndex[trackId];\n if (!track) {\n const validTracks = Object.keys(fragmentIndex).join(\", \");\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: `Track ${trackId} not found (valid tracks: ${validTracks})`,\n }),\n );\n return;\n }\n\n // JIT uses 1-based segment IDs, fragment index uses 0-based\n const segmentIndex = segmentId - 1;\n const segment = track.segments[segmentIndex];\n if (!segment) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: `Segment ${segmentId} not found`,\n availableSegments: track.segments.length,\n }),\n );\n return;\n }\n\n const contentType =\n extension === \"m4s\" ? \"video/iso.segment\" : \"video/mp4\";\n\n if (includeInit) {\n // .mp4: Stream init segment + media segment (playable file)\n const initSegment = track.initSegment;\n log(\n `Media segment ${segmentId}.mp4: init(offset=${initSegment.offset}, size=${initSegment.size}) + segment(offset=${segment.offset}, size=${segment.size})`,\n );\n sendMultipleByteRanges(\n res,\n trackTaskResult.cachePath,\n [\n { offset: initSegment.offset, size: initSegment.size },\n { offset: segment.offset, size: segment.size },\n ],\n contentType,\n );\n } else {\n // .m4s: Stream only this segment's bytes (moof+mdat)\n const { offset, size } = segment;\n log(\n `Media segment ${segmentId}.m4s: offset=${offset}, size=${size}`,\n );\n sendByteRange(\n res,\n trackTaskResult.cachePath,\n offset,\n size,\n contentType,\n );\n }\n } catch (error) {\n log(`Error serving media segment: ${error}`);\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Failed to generate media segment\",\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n }\n return;\n }\n\n // Unknown endpoint\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Unknown transcode endpoint\" }));\n } catch (error) {\n log(`Unexpected error: ${error}`);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Internal server error\",\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"jitTranscodeMiddleware.js","names":["fragmentIndex: Record<number, TrackFragmentIndex>"],"sources":["../src/jitTranscodeMiddleware.ts"],"sourcesContent":["import { createReadStream, statSync } from \"node:fs\";\nimport type { ServerResponse } from \"node:http\";\nimport path from \"node:path\";\n\nimport type { IncomingMessage, NextFunction } from \"connect\";\nimport debug from \"debug\";\nimport mime from \"mime\";\n\nimport { forbidRelativePaths } from \"./forbidRelativePaths.js\";\n\n/**\n * Type for the TrackFragmentIndex from assets package.\n * Duplicated here to avoid import issues between package and direct imports.\n */\nexport interface TrackFragmentIndex {\n type: \"video\" | \"audio\";\n codec: string;\n duration: number;\n timescale: number;\n startTimeOffsetMs: number;\n initSegment: { offset: number; size: number };\n segments: Array<{ offset: number; size: number; duration: number }>;\n width?: number;\n height?: number;\n channel_count?: number;\n sample_rate?: number;\n}\n\n/**\n * Asset functions required by the JIT middleware.\n * These are injected to support both package imports and direct imports.\n */\nexport interface AssetFunctions {\n generateTrack: (\n cacheRoot: string,\n absolutePath: string,\n trackUrl: string,\n ) => Promise<{ cachePath: string }>;\n generateScrubTrack: (\n cacheRoot: string,\n absolutePath: string,\n ) => Promise<{ cachePath: string }>;\n generateTrackFragmentIndex: (\n cacheRoot: string,\n absolutePath: string,\n ) => Promise<{ cachePath: string }>;\n}\n\nexport interface JitMiddlewareOptions {\n root: string;\n cacheRoot: string;\n /**\n * When true, remote URLs (http/https) are handled locally via ffprobe/ffmpeg\n * rather than passed to a downstream proxy (e.g. recordReplayProxyPlugin).\n * Set to true in dev-projects; leave false (default) in test environments\n * that use recordReplayProxyPlugin to proxy remote URLs to the cloud API.\n */\n handleRemoteUrls?: boolean;\n}\n\n/**\n * Stream a specific byte range from a file.\n * This is used for JIT segment serving where the server extracts the correct bytes.\n */\nexport function sendByteRange(\n res: ServerResponse,\n filePath: string,\n offset: number,\n size: number,\n contentType?: string,\n) {\n const log = debug(\"ef:sendByteRange\");\n const stats = statSync(filePath);\n const end = offset + size - 1;\n\n if (end >= stats.size) {\n log(`Requested range ${offset}-${end} exceeds file size ${stats.size}`);\n res.writeHead(416, { \"Content-Range\": `bytes */${stats.size}` });\n res.end();\n return;\n }\n\n log(`Streaming bytes ${offset}-${end} (${size} bytes) from ${filePath}`);\n\n res.writeHead(200, {\n \"Content-Type\": contentType || mime.getType(filePath) || \"video/mp4\",\n \"Content-Length\": size,\n \"Cache-Control\": \"public, max-age=3600\",\n });\n\n const readStream = createReadStream(filePath, { start: offset, end });\n readStream.pipe(res);\n}\n\n/**\n * Stream multiple byte ranges from a file concatenated together.\n * Used for creating playable .mp4 files by combining init segment + media segment.\n */\nexport function sendMultipleByteRanges(\n res: ServerResponse,\n filePath: string,\n ranges: Array<{ offset: number; size: number }>,\n contentType?: string,\n) {\n const log = debug(\"ef:sendMultipleByteRanges\");\n const stats = statSync(filePath);\n\n // Validate all ranges\n for (const range of ranges) {\n const end = range.offset + range.size - 1;\n if (end >= stats.size) {\n log(\n `Requested range ${range.offset}-${end} exceeds file size ${stats.size}`,\n );\n res.writeHead(416, { \"Content-Range\": `bytes */${stats.size}` });\n res.end();\n return;\n }\n }\n\n const totalSize = ranges.reduce((sum, r) => sum + r.size, 0);\n log(\n `Streaming ${ranges.length} ranges (${totalSize} total bytes) from ${filePath}`,\n );\n\n res.writeHead(200, {\n \"Content-Type\": contentType || \"video/mp4\",\n \"Content-Length\": totalSize,\n \"Cache-Control\": \"public, max-age=3600\",\n });\n\n // Stream ranges sequentially\n let rangeIndex = 0;\n\n const streamNextRange = () => {\n if (rangeIndex >= ranges.length) {\n res.end();\n return;\n }\n\n const range = ranges[rangeIndex]!;\n const end = range.offset + range.size - 1;\n const readStream = createReadStream(filePath, { start: range.offset, end });\n\n readStream.on(\"end\", () => {\n rangeIndex++;\n streamNextRange();\n });\n\n readStream.on(\"error\", (err) => {\n log(`Error streaming range ${rangeIndex}: ${err}`);\n res.destroy();\n });\n\n readStream.pipe(res, { end: false });\n };\n\n streamNextRange();\n}\n\n/**\n * Check if a hostname refers to the local vite server.\n * Handles various local hostname patterns including worktree domains.\n */\nexport function isLocalHost(hostname: string): boolean {\n const localPatterns = [\n \"localhost\",\n \"127.0.0.1\",\n \"0.0.0.0\",\n \".localhost\", // Matches *.localhost (worktree domains like main.localhost)\n ];\n\n const lowerHost = hostname.toLowerCase();\n return localPatterns.some((pattern) =>\n pattern.startsWith(\".\")\n ? lowerHost.endsWith(pattern) || lowerHost === pattern.slice(1)\n : lowerHost === pattern || lowerHost.startsWith(pattern + \":\"),\n );\n}\n\n/**\n * Resolve a URL to either a remote URL (for ffprobe) or a local file path.\n *\n * - Remote URLs (different host): passed directly to ffprobe (it supports http/https)\n * - Local URLs (localhost, *.localhost): resolved to local file path\n *\n * @param urlParam - The URL from the query parameter\n * @param root - The vite plugin root directory\n * @returns The path/URL to pass to ffprobe\n */\nexport function resolveMediaPath(urlParam: string, root: string): string {\n try {\n const url = new URL(urlParam);\n const hostname = url.hostname;\n\n // If NOT a local URL, pass directly to ffprobe - it supports http/https URLs\n if (!isLocalHost(hostname)) {\n return urlParam;\n }\n\n // Local URL - resolve to file path\n let filePath = decodeURIComponent(url.pathname);\n\n // Remove leading slash for path.join\n if (filePath.startsWith(\"/\")) {\n filePath = filePath.slice(1);\n }\n\n // The root is already dev-projects/src, so if the URL path starts with \"src/\",\n // we should remove it to avoid duplication (src/src/assets -> src/assets)\n if (filePath.startsWith(\"src/\")) {\n filePath = filePath.slice(4); // Remove \"src/\"\n }\n\n return path.join(root, filePath);\n } catch {\n // If not a valid URL, treat as relative path\n let filePath = urlParam;\n if (filePath.startsWith(\"src/\")) {\n filePath = filePath.slice(4);\n }\n return path.join(root, filePath);\n }\n}\n\n/**\n * Convert fragment index segments to millisecond durations array.\n */\nexport function getSegmentDurationsMs(track: TrackFragmentIndex): number[] {\n return track.segments.map(\n (segment) => (segment.duration / track.timescale) * 1000,\n );\n}\n\n/**\n * Generate a JIT manifest for a local file.\n * Uses actual fragment index data for accurate segment information.\n */\nexport async function generateLocalJitManifest(\n absolutePath: string,\n sourceUrl: string,\n baseUrl: string,\n cacheRoot: string,\n assetFunctions: AssetFunctions,\n) {\n const log = debug(\"ef:generateLocalJitManifest\");\n\n // Generate the fragment index (this also ensures tracks are generated)\n log(`Generating fragment index for ${absolutePath}`);\n const fragmentIndexResult = await assetFunctions.generateTrackFragmentIndex(\n cacheRoot,\n absolutePath,\n );\n const fragmentIndex: Record<number, TrackFragmentIndex> = JSON.parse(\n await import(\"node:fs/promises\").then((fs) =>\n fs.readFile(fragmentIndexResult.cachePath, \"utf-8\"),\n ),\n );\n\n // Find video track (track 1) and audio track (track 2) and scrub track (-1)\n const videoTrack = fragmentIndex[1];\n const audioTrack = fragmentIndex[2];\n const scrubTrack = fragmentIndex[-1];\n\n const hasVideo = videoTrack?.type === \"video\";\n const hasAudio = audioTrack?.type === \"audio\";\n\n // Get duration from the longest track\n let durationMs = 0;\n if (hasVideo && videoTrack) {\n durationMs = Math.max(\n durationMs,\n (videoTrack.duration / videoTrack.timescale) * 1000,\n );\n }\n if (hasAudio && audioTrack) {\n durationMs = Math.max(\n durationMs,\n (audioTrack.duration / audioTrack.timescale) * 1000,\n );\n }\n const durationSeconds = durationMs / 1000;\n\n // Get video dimensions from track\n const width =\n hasVideo && videoTrack && \"width\" in videoTrack ? videoTrack.width : 1920;\n const height =\n hasVideo && videoTrack && \"height\" in videoTrack ? videoTrack.height : 1080;\n const codec = hasVideo && videoTrack ? videoTrack.codec : \"avc1.640029\";\n\n // Get actual segment durations from fragment index\n const videoSegmentDurationsMs =\n hasVideo && videoTrack ? getSegmentDurationsMs(videoTrack) : [];\n const scrubSegmentDurationsMs = scrubTrack\n ? getSegmentDurationsMs(scrubTrack)\n : [];\n const audioSegmentDurationsMs =\n hasAudio && audioTrack ? getSegmentDurationsMs(audioTrack) : [];\n\n // Average segment duration for backward compatibility\n const avgVideoSegmentDurationMs =\n videoSegmentDurationsMs.length > 0\n ? videoSegmentDurationsMs.reduce((a, b) => a + b, 0) /\n videoSegmentDurationsMs.length\n : 2000;\n const avgScrubSegmentDurationMs =\n scrubSegmentDurationsMs.length > 0\n ? scrubSegmentDurationsMs.reduce((a, b) => a + b, 0) /\n scrubSegmentDurationsMs.length\n : 30000;\n const avgAudioSegmentDurationMs =\n audioSegmentDurationsMs.length > 0\n ? audioSegmentDurationsMs.reduce((a, b) => a + b, 0) /\n audioSegmentDurationsMs.length\n : 2000;\n\n log(\n `Video: ${videoSegmentDurationsMs.length} segments, Audio: ${audioSegmentDurationsMs.length} segments, Scrub: ${scrubSegmentDurationsMs.length} segments`,\n );\n\n // Construct manifest matching ManifestResponse format\n const manifest = {\n version: \"1.0\",\n type: \"com.editframe/local-jit-manifest\",\n sourceUrl: sourceUrl,\n duration: durationSeconds,\n durationMs: durationMs,\n baseUrl: baseUrl,\n\n videoRenditions: hasVideo\n ? [\n {\n id: \"high\",\n width: width,\n height: height,\n bitrate: 5000000,\n codec: codec,\n container: \"video/mp4\",\n mimeType: `video/mp4; codecs=\"${codec}\"`,\n segmentDuration: avgVideoSegmentDurationMs / 1000,\n segmentDurationMs: avgVideoSegmentDurationMs,\n segmentDurationsMs: videoSegmentDurationsMs,\n startTimeOffsetMs: videoTrack!.startTimeOffsetMs,\n frameRate: 30,\n profile: \"High\",\n level: \"4.1\",\n },\n ...(scrubTrack\n ? [\n {\n id: \"scrub\",\n width: 320,\n height: Math.round(\n (320 * (height ?? 1080)) / (width ?? 1920),\n ),\n bitrate: 100000,\n codec: scrubTrack.codec,\n container: \"video/mp4\",\n mimeType: `video/mp4; codecs=\"${scrubTrack.codec}\"`,\n segmentDuration: avgScrubSegmentDurationMs / 1000,\n segmentDurationMs: avgScrubSegmentDurationMs,\n segmentDurationsMs: scrubSegmentDurationsMs,\n startTimeOffsetMs: scrubTrack.startTimeOffsetMs,\n frameRate: 15,\n profile: \"High\",\n level: \"4.1\",\n },\n ]\n : []),\n ]\n : [],\n\n audioRenditions:\n hasAudio && audioTrack\n ? [\n {\n id: \"audio\",\n channels:\n \"channel_count\" in audioTrack ? audioTrack.channel_count : 2,\n sampleRate:\n \"sample_rate\" in audioTrack ? audioTrack.sample_rate : 48000,\n bitrate: 128000,\n codec: audioTrack.codec,\n container: \"audio/mp4\",\n mimeType: `audio/mp4; codecs=\"${audioTrack.codec}\"`,\n segmentDuration: avgAudioSegmentDurationMs / 1000,\n segmentDurationMs: avgAudioSegmentDurationMs,\n segmentDurationsMs: audioSegmentDurationsMs,\n language: \"en\",\n },\n ]\n : [],\n\n endpoints: {\n initSegment: `${baseUrl}/api/v1/transcode/{rendition}/init.m4s?url=${encodeURIComponent(sourceUrl)}`,\n mediaSegment: `${baseUrl}/api/v1/transcode/{rendition}/{segmentId}.m4s?url=${encodeURIComponent(sourceUrl)}`,\n },\n\n jitInfo: {\n parallelTranscodingSupported: true,\n expectedTranscodeLatency: 100, // Local is fast\n segmentCount: videoSegmentDurationsMs.length,\n scrubSegmentCount: scrubSegmentDurationsMs.length,\n },\n };\n\n return manifest;\n}\n\n/**\n * Create the JIT transcode middleware for /api/v1/transcode/* routes.\n *\n * @param options - The middleware options (root, cacheRoot)\n * @param assetFunctions - The asset functions to use (allows dependency injection)\n * @returns Express-compatible middleware function\n */\nexport function createJitTranscodeMiddleware(\n options: JitMiddlewareOptions,\n assetFunctions: AssetFunctions,\n) {\n return async (\n req: IncomingMessage,\n res: ServerResponse,\n next: NextFunction,\n ) => {\n const log = debug(\"ef:vite-plugin:jit\");\n\n if (!req.url?.startsWith(\"/api/v1/transcode/\")) {\n return next();\n }\n\n forbidRelativePaths(req);\n log(`Handling JIT transcode request: ${req.url}`);\n\n const url = new URL(req.url, `http://${req.headers.host}`);\n const sourceUrl = url.searchParams.get(\"url\");\n\n if (!sourceUrl) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"url parameter is required\" }));\n return;\n }\n\n // Resolve URL to either local file path or keep as remote URL (ffprobe supports both)\n const mediaPath = resolveMediaPath(sourceUrl, options.root);\n\n // If the source is a remote URL and handleRemoteUrls is not enabled, let the\n // next middleware handle it (e.g. recordReplayProxyPlugin in test environments).\n if (\n !options.handleRemoteUrls &&\n (mediaPath.startsWith(\"http://\") || mediaPath.startsWith(\"https://\"))\n ) {\n return next();\n }\n\n // Handle CORS\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization\",\n );\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n try {\n // Parse the path to determine which endpoint\n const pathMatch = url.pathname.match(\n /^\\/api\\/v1\\/transcode\\/(?:([^/]+)\\/)?(.+)$/,\n );\n\n if (!pathMatch) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid transcode endpoint\" }));\n return;\n }\n\n const [, rendition, endpoint] = pathMatch;\n\n // Handle manifest.json endpoint\n if (endpoint === \"manifest.json\") {\n log(`Generating manifest for ${mediaPath}`);\n const proto =\n (req.headers[\"x-forwarded-proto\"] as string | undefined) ||\n url.protocol.replace(\":\", \"\");\n const baseUrl = `${proto}://${url.host}`;\n\n try {\n const manifest = await generateLocalJitManifest(\n mediaPath,\n sourceUrl,\n baseUrl,\n options.cacheRoot,\n assetFunctions,\n );\n\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"public, max-age=300\",\n });\n res.end(JSON.stringify(manifest, null, 2));\n } catch (error) {\n log(`Error generating manifest: ${error}`);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Failed to generate manifest\",\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n return;\n }\n\n // Get fragment index first - we need it to determine track IDs\n const fragmentIndexResult =\n await assetFunctions.generateTrackFragmentIndex(\n options.cacheRoot,\n mediaPath,\n );\n const fragmentIndex: Record<number, TrackFragmentIndex> = JSON.parse(\n await import(\"node:fs/promises\").then((fs) =>\n fs.readFile(fragmentIndexResult.cachePath, \"utf-8\"),\n ),\n );\n\n // Helper: Map rendition ID to track ID, using fragment index to find correct track\n // For video files: track 1 = video, track 2 = audio\n // For audio-only files: track 1 = audio (no track 2)\n const getTrackId = (renditionId: string): number => {\n if (renditionId === \"scrub\") return -1;\n\n if (renditionId === \"audio\") {\n // Find the audio track - could be track 1 (audio-only) or track 2 (video+audio)\n for (const [trackIdStr, trackInfo] of Object.entries(fragmentIndex)) {\n if (trackInfo.type === \"audio\") {\n return Number.parseInt(trackIdStr, 10);\n }\n }\n // Fallback to track 2 if no audio track found by type\n return 2;\n }\n\n // For video renditions (high, medium, low), find the video track\n for (const [trackIdStr, trackInfo] of Object.entries(fragmentIndex)) {\n if (trackInfo.type === \"video\") {\n return Number.parseInt(trackIdStr, 10);\n }\n }\n // Fallback to track 1\n return 1;\n };\n\n // Handle init segment endpoint (e.g., /api/v1/transcode/high/init.mp4 or init.m4s)\n const initMatch = endpoint?.match(/^init\\.(mp4|m4s)$/);\n if (initMatch && rendition) {\n const extension = initMatch[1];\n const contentType =\n extension === \"m4s\" ? \"video/iso.segment\" : \"video/mp4\";\n log(\n `Serving init segment (${extension}) for ${mediaPath}, rendition: ${rendition}`,\n );\n\n try {\n const trackId = getTrackId(rendition);\n\n // Generate/get the track file\n let trackTaskResult;\n if (trackId === -1) {\n trackTaskResult = await assetFunctions.generateScrubTrack(\n options.cacheRoot,\n mediaPath,\n );\n } else {\n const trackUrl = `/@ef-track/${mediaPath}?trackId=${trackId}`;\n trackTaskResult = await assetFunctions.generateTrack(\n options.cacheRoot,\n mediaPath,\n trackUrl,\n );\n }\n\n const track = fragmentIndex[trackId];\n if (!track) {\n const validTracks = Object.keys(fragmentIndex).join(\", \");\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: `Track ${trackId} not found (valid tracks: ${validTracks})`,\n }),\n );\n return;\n }\n\n // Stream only the init segment bytes\n const { offset, size } = track.initSegment;\n log(`Init segment: offset=${offset}, size=${size}`);\n sendByteRange(\n res,\n trackTaskResult.cachePath,\n offset,\n size,\n contentType,\n );\n } catch (error) {\n log(`Error serving init segment: ${error}`);\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Failed to generate init segment\",\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n }\n return;\n }\n\n // Handle media segment endpoint\n // - .m4s: moof+mdat only (fragment for streaming)\n // - .mp4: init+moof+mdat (playable standalone file for testing)\n const segmentMatch = endpoint?.match(/^(\\d+)\\.(mp4|m4s)$/);\n if (segmentMatch?.[1] && segmentMatch?.[2] && rendition) {\n const segmentId = Number.parseInt(segmentMatch[1], 10);\n const extension = segmentMatch[2];\n const includeInit = extension === \"mp4\";\n log(\n `Serving media segment ${segmentId}.${extension} for ${mediaPath}, rendition: ${rendition}, includeInit: ${includeInit}`,\n );\n\n try {\n const trackId = getTrackId(rendition);\n\n // Generate/get the track file\n let trackTaskResult;\n if (trackId === -1) {\n trackTaskResult = await assetFunctions.generateScrubTrack(\n options.cacheRoot,\n mediaPath,\n );\n } else {\n const trackUrl = `/@ef-track/${mediaPath}?trackId=${trackId}`;\n trackTaskResult = await assetFunctions.generateTrack(\n options.cacheRoot,\n mediaPath,\n trackUrl,\n );\n }\n\n const track = fragmentIndex[trackId];\n if (!track) {\n const validTracks = Object.keys(fragmentIndex).join(\", \");\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: `Track ${trackId} not found (valid tracks: ${validTracks})`,\n }),\n );\n return;\n }\n\n // JIT uses 1-based segment IDs, fragment index uses 0-based\n const segmentIndex = segmentId - 1;\n const segment = track.segments[segmentIndex];\n if (!segment) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: `Segment ${segmentId} not found`,\n availableSegments: track.segments.length,\n }),\n );\n return;\n }\n\n const contentType =\n extension === \"m4s\" ? \"video/iso.segment\" : \"video/mp4\";\n\n if (includeInit) {\n // .mp4: Stream init segment + media segment (playable file)\n const initSegment = track.initSegment;\n log(\n `Media segment ${segmentId}.mp4: init(offset=${initSegment.offset}, size=${initSegment.size}) + segment(offset=${segment.offset}, size=${segment.size})`,\n );\n sendMultipleByteRanges(\n res,\n trackTaskResult.cachePath,\n [\n { offset: initSegment.offset, size: initSegment.size },\n { offset: segment.offset, size: segment.size },\n ],\n contentType,\n );\n } else {\n // .m4s: Stream only this segment's bytes (moof+mdat)\n const { offset, size } = segment;\n log(\n `Media segment ${segmentId}.m4s: offset=${offset}, size=${size}`,\n );\n sendByteRange(\n res,\n trackTaskResult.cachePath,\n offset,\n size,\n contentType,\n );\n }\n } catch (error) {\n log(`Error serving media segment: ${error}`);\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"File not found\");\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Failed to generate media segment\",\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n }\n return;\n }\n\n // Unknown endpoint\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Unknown transcode endpoint\" }));\n } catch (error) {\n log(`Unexpected error: ${error}`);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Internal server error\",\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n };\n}\n"],"mappings":";;;;;;;;;;;AAgEA,SAAgB,cACd,KACA,UACA,QACA,MACA,aACA;CACA,MAAM,MAAM,MAAM,mBAAmB;CACrC,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,MAAM,SAAS,OAAO;AAE5B,KAAI,OAAO,MAAM,MAAM;AACrB,MAAI,mBAAmB,OAAO,GAAG,IAAI,qBAAqB,MAAM,OAAO;AACvE,MAAI,UAAU,KAAK,EAAE,iBAAiB,WAAW,MAAM,QAAQ,CAAC;AAChE,MAAI,KAAK;AACT;;AAGF,KAAI,mBAAmB,OAAO,GAAG,IAAI,IAAI,KAAK,eAAe,WAAW;AAExE,KAAI,UAAU,KAAK;EACjB,gBAAgB,eAAe,KAAK,QAAQ,SAAS,IAAI;EACzD,kBAAkB;EAClB,iBAAiB;EAClB,CAAC;AAGF,CADmB,iBAAiB,UAAU;EAAE,OAAO;EAAQ;EAAK,CAAC,CAC1D,KAAK,IAAI;;;;;;AAOtB,SAAgB,uBACd,KACA,UACA,QACA,aACA;CACA,MAAM,MAAM,MAAM,4BAA4B;CAC9C,MAAM,QAAQ,SAAS,SAAS;AAGhC,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM,SAAS,MAAM,OAAO;AACxC,MAAI,OAAO,MAAM,MAAM;AACrB,OACE,mBAAmB,MAAM,OAAO,GAAG,IAAI,qBAAqB,MAAM,OACnE;AACD,OAAI,UAAU,KAAK,EAAE,iBAAiB,WAAW,MAAM,QAAQ,CAAC;AAChE,OAAI,KAAK;AACT;;;CAIJ,MAAM,YAAY,OAAO,QAAQ,KAAK,MAAM,MAAM,EAAE,MAAM,EAAE;AAC5D,KACE,aAAa,OAAO,OAAO,WAAW,UAAU,qBAAqB,WACtE;AAED,KAAI,UAAU,KAAK;EACjB,gBAAgB,eAAe;EAC/B,kBAAkB;EAClB,iBAAiB;EAClB,CAAC;CAGF,IAAI,aAAa;CAEjB,MAAM,wBAAwB;AAC5B,MAAI,cAAc,OAAO,QAAQ;AAC/B,OAAI,KAAK;AACT;;EAGF,MAAM,QAAQ,OAAO;EACrB,MAAM,MAAM,MAAM,SAAS,MAAM,OAAO;EACxC,MAAM,aAAa,iBAAiB,UAAU;GAAE,OAAO,MAAM;GAAQ;GAAK,CAAC;AAE3E,aAAW,GAAG,aAAa;AACzB;AACA,oBAAiB;IACjB;AAEF,aAAW,GAAG,UAAU,QAAQ;AAC9B,OAAI,yBAAyB,WAAW,IAAI,MAAM;AAClD,OAAI,SAAS;IACb;AAEF,aAAW,KAAK,KAAK,EAAE,KAAK,OAAO,CAAC;;AAGtC,kBAAiB;;;;;;AAOnB,SAAgB,YAAY,UAA2B;CACrD,MAAM,gBAAgB;EACpB;EACA;EACA;EACA;EACD;CAED,MAAM,YAAY,SAAS,aAAa;AACxC,QAAO,cAAc,MAAM,YACzB,QAAQ,WAAW,IAAI,GACnB,UAAU,SAAS,QAAQ,IAAI,cAAc,QAAQ,MAAM,EAAE,GAC7D,cAAc,WAAW,UAAU,WAAW,UAAU,IAAI,CACjE;;;;;;;;;;;;AAaH,SAAgB,iBAAiB,UAAkB,MAAsB;AACvE,KAAI;EACF,MAAM,MAAM,IAAI,IAAI,SAAS;EAC7B,MAAM,WAAW,IAAI;AAGrB,MAAI,CAAC,YAAY,SAAS,CACxB,QAAO;EAIT,IAAI,WAAW,mBAAmB,IAAI,SAAS;AAG/C,MAAI,SAAS,WAAW,IAAI,CAC1B,YAAW,SAAS,MAAM,EAAE;AAK9B,MAAI,SAAS,WAAW,OAAO,CAC7B,YAAW,SAAS,MAAM,EAAE;AAG9B,SAAO,KAAK,KAAK,MAAM,SAAS;SAC1B;EAEN,IAAI,WAAW;AACf,MAAI,SAAS,WAAW,OAAO,CAC7B,YAAW,SAAS,MAAM,EAAE;AAE9B,SAAO,KAAK,KAAK,MAAM,SAAS;;;;;;AAOpC,SAAgB,sBAAsB,OAAqC;AACzE,QAAO,MAAM,SAAS,KACnB,YAAa,QAAQ,WAAW,MAAM,YAAa,IACrD;;;;;;AAOH,eAAsB,yBACpB,cACA,WACA,SACA,WACA,gBACA;CACA,MAAM,MAAM,MAAM,8BAA8B;AAGhD,KAAI,iCAAiC,eAAe;CACpD,MAAM,sBAAsB,MAAM,eAAe,2BAC/C,WACA,aACD;CACD,MAAMA,gBAAoD,KAAK,MAC7D,MAAM,OAAO,oBAAoB,MAAM,OACrC,GAAG,SAAS,oBAAoB,WAAW,QAAQ,CACpD,CACF;CAGD,MAAM,aAAa,cAAc;CACjC,MAAM,aAAa,cAAc;CACjC,MAAM,aAAa,cAAc;CAEjC,MAAM,WAAW,YAAY,SAAS;CACtC,MAAM,WAAW,YAAY,SAAS;CAGtC,IAAI,aAAa;AACjB,KAAI,YAAY,WACd,cAAa,KAAK,IAChB,YACC,WAAW,WAAW,WAAW,YAAa,IAChD;AAEH,KAAI,YAAY,WACd,cAAa,KAAK,IAChB,YACC,WAAW,WAAW,WAAW,YAAa,IAChD;CAEH,MAAM,kBAAkB,aAAa;CAGrC,MAAM,QACJ,YAAY,cAAc,WAAW,aAAa,WAAW,QAAQ;CACvE,MAAM,SACJ,YAAY,cAAc,YAAY,aAAa,WAAW,SAAS;CACzE,MAAM,QAAQ,YAAY,aAAa,WAAW,QAAQ;CAG1D,MAAM,0BACJ,YAAY,aAAa,sBAAsB,WAAW,GAAG,EAAE;CACjE,MAAM,0BAA0B,aAC5B,sBAAsB,WAAW,GACjC,EAAE;CACN,MAAM,0BACJ,YAAY,aAAa,sBAAsB,WAAW,GAAG,EAAE;CAGjE,MAAM,4BACJ,wBAAwB,SAAS,IAC7B,wBAAwB,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAClD,wBAAwB,SACxB;CACN,MAAM,4BACJ,wBAAwB,SAAS,IAC7B,wBAAwB,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAClD,wBAAwB,SACxB;CACN,MAAM,4BACJ,wBAAwB,SAAS,IAC7B,wBAAwB,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAClD,wBAAwB,SACxB;AAEN,KACE,UAAU,wBAAwB,OAAO,oBAAoB,wBAAwB,OAAO,oBAAoB,wBAAwB,OAAO,WAChJ;AAwFD,QArFiB;EACf,SAAS;EACT,MAAM;EACK;EACX,UAAU;EACE;EACH;EAET,iBAAiB,WACb,CACE;GACE,IAAI;GACG;GACC;GACR,SAAS;GACF;GACP,WAAW;GACX,UAAU,sBAAsB,MAAM;GACtC,iBAAiB,4BAA4B;GAC7C,mBAAmB;GACnB,oBAAoB;GACpB,mBAAmB,WAAY;GAC/B,WAAW;GACX,SAAS;GACT,OAAO;GACR,EACD,GAAI,aACA,CACE;GACE,IAAI;GACJ,OAAO;GACP,QAAQ,KAAK,MACV,OAAO,UAAU,SAAU,SAAS,MACtC;GACD,SAAS;GACT,OAAO,WAAW;GAClB,WAAW;GACX,UAAU,sBAAsB,WAAW,MAAM;GACjD,iBAAiB,4BAA4B;GAC7C,mBAAmB;GACnB,oBAAoB;GACpB,mBAAmB,WAAW;GAC9B,WAAW;GACX,SAAS;GACT,OAAO;GACR,CACF,GACD,EAAE,CACP,GACD,EAAE;EAEN,iBACE,YAAY,aACR,CACE;GACE,IAAI;GACJ,UACE,mBAAmB,aAAa,WAAW,gBAAgB;GAC7D,YACE,iBAAiB,aAAa,WAAW,cAAc;GACzD,SAAS;GACT,OAAO,WAAW;GAClB,WAAW;GACX,UAAU,sBAAsB,WAAW,MAAM;GACjD,iBAAiB,4BAA4B;GAC7C,mBAAmB;GACnB,oBAAoB;GACpB,UAAU;GACX,CACF,GACD,EAAE;EAER,WAAW;GACT,aAAa,GAAG,QAAQ,6CAA6C,mBAAmB,UAAU;GAClG,cAAc,GAAG,QAAQ,oDAAoD,mBAAmB,UAAU;GAC3G;EAED,SAAS;GACP,8BAA8B;GAC9B,0BAA0B;GAC1B,cAAc,wBAAwB;GACtC,mBAAmB,wBAAwB;GAC5C;EACF;;;;;;;;;AAYH,SAAgB,6BACd,SACA,gBACA;AACA,QAAO,OACL,KACA,KACA,SACG;EACH,MAAM,MAAM,MAAM,qBAAqB;AAEvC,MAAI,CAAC,IAAI,KAAK,WAAW,qBAAqB,CAC5C,QAAO,MAAM;AAGf,sBAAoB,IAAI;AACxB,MAAI,mCAAmC,IAAI,MAAM;EAEjD,MAAM,MAAM,IAAI,IAAI,IAAI,KAAK,UAAU,IAAI,QAAQ,OAAO;EAC1D,MAAM,YAAY,IAAI,aAAa,IAAI,MAAM;AAE7C,MAAI,CAAC,WAAW;AACd,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU,EAAE,OAAO,6BAA6B,CAAC,CAAC;AAC/D;;EAIF,MAAM,YAAY,iBAAiB,WAAW,QAAQ,KAAK;AAI3D,MACE,CAAC,QAAQ,qBACR,UAAU,WAAW,UAAU,IAAI,UAAU,WAAW,WAAW,EAEpE,QAAO,MAAM;AAIf,MAAI,UAAU,+BAA+B,IAAI;AACjD,MAAI,UAAU,gCAAgC,eAAe;AAC7D,MAAI,UACF,gCACA,8BACD;AAED,MAAI,IAAI,WAAW,WAAW;AAC5B,OAAI,UAAU,IAAI;AAClB,OAAI,KAAK;AACT;;AAGF,MAAI;GAEF,MAAM,YAAY,IAAI,SAAS,MAC7B,6CACD;AAED,OAAI,CAAC,WAAW;AACd,QAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,QAAI,IAAI,KAAK,UAAU,EAAE,OAAO,8BAA8B,CAAC,CAAC;AAChE;;GAGF,MAAM,GAAG,WAAW,YAAY;AAGhC,OAAI,aAAa,iBAAiB;AAChC,QAAI,2BAA2B,YAAY;IAI3C,MAAM,UAAU,GAFb,IAAI,QAAQ,wBACb,IAAI,SAAS,QAAQ,KAAK,GAAG,CACN,KAAK,IAAI;AAElC,QAAI;KACF,MAAM,WAAW,MAAM,yBACrB,WACA,WACA,SACA,QAAQ,WACR,eACD;AAED,SAAI,UAAU,KAAK;MACjB,gBAAgB;MAChB,iBAAiB;MAClB,CAAC;AACF,SAAI,IAAI,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC;aACnC,OAAO;AACd,SAAI,8BAA8B,QAAQ;AAC1C,SAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,SAAI,IACF,KAAK,UAAU;MACb,OAAO;MACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;MAChE,CAAC,CACH;;AAEH;;GAIF,MAAM,sBACJ,MAAM,eAAe,2BACnB,QAAQ,WACR,UACD;GACH,MAAMA,gBAAoD,KAAK,MAC7D,MAAM,OAAO,oBAAoB,MAAM,OACrC,GAAG,SAAS,oBAAoB,WAAW,QAAQ,CACpD,CACF;GAKD,MAAM,cAAc,gBAAgC;AAClD,QAAI,gBAAgB,QAAS,QAAO;AAEpC,QAAI,gBAAgB,SAAS;AAE3B,UAAK,MAAM,CAAC,YAAY,cAAc,OAAO,QAAQ,cAAc,CACjE,KAAI,UAAU,SAAS,QACrB,QAAO,OAAO,SAAS,YAAY,GAAG;AAI1C,YAAO;;AAIT,SAAK,MAAM,CAAC,YAAY,cAAc,OAAO,QAAQ,cAAc,CACjE,KAAI,UAAU,SAAS,QACrB,QAAO,OAAO,SAAS,YAAY,GAAG;AAI1C,WAAO;;GAIT,MAAM,YAAY,UAAU,MAAM,oBAAoB;AACtD,OAAI,aAAa,WAAW;IAC1B,MAAM,YAAY,UAAU;IAC5B,MAAM,cACJ,cAAc,QAAQ,sBAAsB;AAC9C,QACE,yBAAyB,UAAU,QAAQ,UAAU,eAAe,YACrE;AAED,QAAI;KACF,MAAM,UAAU,WAAW,UAAU;KAGrC,IAAI;AACJ,SAAI,YAAY,GACd,mBAAkB,MAAM,eAAe,mBACrC,QAAQ,WACR,UACD;UACI;MACL,MAAM,WAAW,cAAc,UAAU,WAAW;AACpD,wBAAkB,MAAM,eAAe,cACrC,QAAQ,WACR,WACA,SACD;;KAGH,MAAM,QAAQ,cAAc;AAC5B,SAAI,CAAC,OAAO;MACV,MAAM,cAAc,OAAO,KAAK,cAAc,CAAC,KAAK,KAAK;AACzD,UAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,UAAI,IACF,KAAK,UAAU,EACb,OAAO,SAAS,QAAQ,4BAA4B,YAAY,IACjE,CAAC,CACH;AACD;;KAIF,MAAM,EAAE,QAAQ,SAAS,MAAM;AAC/B,SAAI,wBAAwB,OAAO,SAAS,OAAO;AACnD,mBACE,KACA,gBAAgB,WAChB,QACA,MACA,YACD;aACM,OAAO;AACd,SAAI,+BAA+B,QAAQ;AAC3C,SAAK,MAAgC,SAAS,UAAU;AACtD,UAAI,UAAU,KAAK,EAAE,gBAAgB,cAAc,CAAC;AACpD,UAAI,IAAI,iBAAiB;YACpB;AACL,UAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,UAAI,IACF,KAAK,UAAU;OACb,OAAO;OACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;OAChE,CAAC,CACH;;;AAGL;;GAMF,MAAM,eAAe,UAAU,MAAM,qBAAqB;AAC1D,OAAI,eAAe,MAAM,eAAe,MAAM,WAAW;IACvD,MAAM,YAAY,OAAO,SAAS,aAAa,IAAI,GAAG;IACtD,MAAM,YAAY,aAAa;IAC/B,MAAM,cAAc,cAAc;AAClC,QACE,yBAAyB,UAAU,GAAG,UAAU,OAAO,UAAU,eAAe,UAAU,iBAAiB,cAC5G;AAED,QAAI;KACF,MAAM,UAAU,WAAW,UAAU;KAGrC,IAAI;AACJ,SAAI,YAAY,GACd,mBAAkB,MAAM,eAAe,mBACrC,QAAQ,WACR,UACD;UACI;MACL,MAAM,WAAW,cAAc,UAAU,WAAW;AACpD,wBAAkB,MAAM,eAAe,cACrC,QAAQ,WACR,WACA,SACD;;KAGH,MAAM,QAAQ,cAAc;AAC5B,SAAI,CAAC,OAAO;MACV,MAAM,cAAc,OAAO,KAAK,cAAc,CAAC,KAAK,KAAK;AACzD,UAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,UAAI,IACF,KAAK,UAAU,EACb,OAAO,SAAS,QAAQ,4BAA4B,YAAY,IACjE,CAAC,CACH;AACD;;KAIF,MAAM,eAAe,YAAY;KACjC,MAAM,UAAU,MAAM,SAAS;AAC/B,SAAI,CAAC,SAAS;AACZ,UAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,UAAI,IACF,KAAK,UAAU;OACb,OAAO,WAAW,UAAU;OAC5B,mBAAmB,MAAM,SAAS;OACnC,CAAC,CACH;AACD;;KAGF,MAAM,cACJ,cAAc,QAAQ,sBAAsB;AAE9C,SAAI,aAAa;MAEf,MAAM,cAAc,MAAM;AAC1B,UACE,iBAAiB,UAAU,oBAAoB,YAAY,OAAO,SAAS,YAAY,KAAK,qBAAqB,QAAQ,OAAO,SAAS,QAAQ,KAAK,GACvJ;AACD,6BACE,KACA,gBAAgB,WAChB,CACE;OAAE,QAAQ,YAAY;OAAQ,MAAM,YAAY;OAAM,EACtD;OAAE,QAAQ,QAAQ;OAAQ,MAAM,QAAQ;OAAM,CAC/C,EACD,YACD;YACI;MAEL,MAAM,EAAE,QAAQ,SAAS;AACzB,UACE,iBAAiB,UAAU,eAAe,OAAO,SAAS,OAC3D;AACD,oBACE,KACA,gBAAgB,WAChB,QACA,MACA,YACD;;aAEI,OAAO;AACd,SAAI,gCAAgC,QAAQ;AAC5C,SAAK,MAAgC,SAAS,UAAU;AACtD,UAAI,UAAU,KAAK,EAAE,gBAAgB,cAAc,CAAC;AACpD,UAAI,IAAI,iBAAiB;YACpB;AACL,UAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,UAAI,IACF,KAAK,UAAU;OACb,OAAO;OACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;OAChE,CAAC,CACH;;;AAGL;;AAIF,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU,EAAE,OAAO,8BAA8B,CAAC,CAAC;WACzD,OAAO;AACd,OAAI,qBAAqB,QAAQ;AACjC,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IACF,KAAK,UAAU;IACb,OAAO;IACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;IAChE,CAAC,CACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/vite-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "Editframe vite plugin",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"author": "",
|
|
21
21
|
"license": "UNLICENSED",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@editframe/api": "0.
|
|
24
|
-
"@editframe/assets": "0.
|
|
23
|
+
"@editframe/api": "0.39.0",
|
|
24
|
+
"@editframe/assets": "0.39.0",
|
|
25
25
|
"connect": "^3.7.0",
|
|
26
26
|
"debug": "^4.3.5",
|
|
27
27
|
"mime": "^4.0.3",
|