@engine-room/after-effects-mcp 0.1.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/LICENSE +21 -0
- package/README.md +176 -0
- package/bin/server.js +1616 -0
- package/package.json +64 -0
- package/panel/CSXS/manifest.xml +49 -0
- package/panel/client/csinterface.js +32 -0
- package/panel/client/index.html +27 -0
- package/panel/client/main.js +371 -0
- package/panel/jsx/boot.jsx +3 -0
- package/panel/jsx/bundle.jsx +1562 -0
- package/panel/package.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@engine-room/after-effects-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server that lets an AI agent drive Adobe After Effects — comps, layers, keyframes, expressions, effects, text, shapes, masks and screenshots.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Engine Room",
|
|
8
|
+
"homepage": "https://github.com/Engine-Room-Games/after-effects-mcp#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Engine-Room-Games/after-effects-mcp.git",
|
|
12
|
+
"directory": "packages/mcp-server"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Engine-Room-Games/after-effects-mcp/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"after-effects",
|
|
21
|
+
"adobe",
|
|
22
|
+
"animation",
|
|
23
|
+
"motion-graphics",
|
|
24
|
+
"extendscript",
|
|
25
|
+
"claude"
|
|
26
|
+
],
|
|
27
|
+
"bin": {
|
|
28
|
+
"after-effects-mcp": "bin/server.js"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"bin",
|
|
35
|
+
"panel",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"os": [
|
|
40
|
+
"darwin"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.json",
|
|
47
|
+
"watch": "tsc -p tsconfig.json --watch",
|
|
48
|
+
"prepack": "node ../../scripts/prepare-package.mjs",
|
|
49
|
+
"clean": "rm -rf dist bin panel"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
53
|
+
"ws": "^8.18.0",
|
|
54
|
+
"zod": "^3.23.8",
|
|
55
|
+
"zod-to-json-schema": "^3.23.5"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@engineroom/shared": "*",
|
|
59
|
+
"@types/node": "^20.14.0",
|
|
60
|
+
"@types/ws": "^8.5.13",
|
|
61
|
+
"esbuild": "^0.28.2",
|
|
62
|
+
"typescript": "^5.6.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<ExtensionManifest Version="11.0" ExtensionBundleId="games.engine-room.ae-mcp" ExtensionBundleVersion="0.1.0"
|
|
3
|
+
ExtensionBundleName="AE MCP Bridge" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
4
|
+
<ExtensionList>
|
|
5
|
+
<Extension Id="games.engine-room.ae-mcp.panel" Version="0.1.0" />
|
|
6
|
+
</ExtensionList>
|
|
7
|
+
<ExecutionEnvironment>
|
|
8
|
+
<HostList>
|
|
9
|
+
<Host Name="AEFT" Version="[22.0,99.9]" />
|
|
10
|
+
</HostList>
|
|
11
|
+
<LocaleList>
|
|
12
|
+
<Locale Code="All" />
|
|
13
|
+
</LocaleList>
|
|
14
|
+
<RequiredRuntimeList>
|
|
15
|
+
<RequiredRuntime Name="CSXS" Version="9.0" />
|
|
16
|
+
</RequiredRuntimeList>
|
|
17
|
+
</ExecutionEnvironment>
|
|
18
|
+
<DispatchInfoList>
|
|
19
|
+
<Extension Id="games.engine-room.ae-mcp.panel">
|
|
20
|
+
<DispatchInfo>
|
|
21
|
+
<Resources>
|
|
22
|
+
<MainPath>./client/index.html</MainPath>
|
|
23
|
+
<ScriptPath>./jsx/boot.jsx</ScriptPath>
|
|
24
|
+
<CEFCommandLine>
|
|
25
|
+
<Parameter>--enable-nodejs</Parameter>
|
|
26
|
+
<Parameter>--mixed-context</Parameter>
|
|
27
|
+
<Parameter>--allow-file-access-from-files</Parameter>
|
|
28
|
+
<Parameter>--allow-file-access</Parameter>
|
|
29
|
+
</CEFCommandLine>
|
|
30
|
+
</Resources>
|
|
31
|
+
<Lifecycle>
|
|
32
|
+
<AutoVisible>true</AutoVisible>
|
|
33
|
+
<StartOn>
|
|
34
|
+
<Event>com.adobe.csxs.events.ApplicationActivate</Event>
|
|
35
|
+
</StartOn>
|
|
36
|
+
</Lifecycle>
|
|
37
|
+
<UI>
|
|
38
|
+
<Type>Panel</Type>
|
|
39
|
+
<Menu>AE MCP Bridge</Menu>
|
|
40
|
+
<Geometry>
|
|
41
|
+
<Size><Height>200</Height><Width>320</Width></Size>
|
|
42
|
+
<MinSize><Height>120</Height><Width>240</Width></MinSize>
|
|
43
|
+
<MaxSize><Height>800</Height><Width>800</Width></MaxSize>
|
|
44
|
+
</Geometry>
|
|
45
|
+
</UI>
|
|
46
|
+
</DispatchInfo>
|
|
47
|
+
</Extension>
|
|
48
|
+
</DispatchInfoList>
|
|
49
|
+
</ExtensionManifest>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Minimal CSInterface — only the methods our panel needs.
|
|
2
|
+
// CEP injects window.__adobe_cep__ which provides the real implementation.
|
|
3
|
+
|
|
4
|
+
function CSInterface() {}
|
|
5
|
+
|
|
6
|
+
CSInterface.prototype.evalScript = function (script, callback) {
|
|
7
|
+
if (callback === null || callback === undefined) callback = function () {};
|
|
8
|
+
window.__adobe_cep__.evalScript(script, callback);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
CSInterface.prototype.getSystemPath = function (pathType) {
|
|
12
|
+
var path = window.__adobe_cep__.getSystemPath(pathType);
|
|
13
|
+
if (path && path.indexOf("file://") === 0) path = decodeURIComponent(path.substring(7));
|
|
14
|
+
return path;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
CSInterface.prototype.getHostEnvironment = function () {
|
|
18
|
+
try { return JSON.parse(window.__adobe_cep__.getHostEnvironment()); } catch (e) { return {}; }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
CSInterface.prototype.openURLInDefaultBrowser = function (url) {
|
|
22
|
+
if (window.__adobe_cep__.openURLInDefaultBrowser) window.__adobe_cep__.openURLInDefaultBrowser(url);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
var SystemPath = {
|
|
26
|
+
USER_DATA: "userData",
|
|
27
|
+
COMMON_FILES: "commonFiles",
|
|
28
|
+
MY_DOCUMENTS: "myDocuments",
|
|
29
|
+
APPLICATION: "application",
|
|
30
|
+
EXTENSION: "extension",
|
|
31
|
+
HOST_APPLICATION: "hostApplication",
|
|
32
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>AE MCP Bridge</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: -apple-system, "Segoe UI", sans-serif; background: #2a2a2a; color: #ddd; padding: 12px; margin: 0; font-size: 12px; }
|
|
8
|
+
h1 { font-size: 13px; margin: 0 0 8px 0; color: #fff; }
|
|
9
|
+
.row { margin: 4px 0; }
|
|
10
|
+
.label { color: #888; }
|
|
11
|
+
.ok { color: #6f6; } .err { color: #f66; } .warn { color: #fc6; }
|
|
12
|
+
#log { margin-top: 8px; font-family: ui-monospace, Menlo, monospace; font-size: 10px; background: #1a1a1a; padding: 6px; border-radius: 4px; max-height: 220px; overflow: auto; }
|
|
13
|
+
#log div { white-space: pre-wrap; }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<h1>AE MCP Bridge</h1>
|
|
18
|
+
<div class="row"><span class="label">Status:</span> <span id="status">starting…</span></div>
|
|
19
|
+
<div class="row"><span class="label">Port:</span> <span id="port">—</span></div>
|
|
20
|
+
<div class="row"><span class="label">AE:</span> <span id="ae">—</span></div>
|
|
21
|
+
<div class="row"><span class="label">JSX:</span> <span id="jsx">—</span></div>
|
|
22
|
+
<div class="row"><span class="label">Requests:</span> <span id="reqs">0</span></div>
|
|
23
|
+
<div id="log"></div>
|
|
24
|
+
<script src="./csinterface.js"></script>
|
|
25
|
+
<script src="./main.js"></script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// main.js — the bridge.
|
|
2
|
+
// Boots inside the CEP panel. Loads bundle.jsx into ExtendScript, then runs
|
|
3
|
+
// a local HTTP+WS server that the Node MCP server connects to.
|
|
4
|
+
|
|
5
|
+
(function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
// CEP exposes Node via mixed-context.
|
|
9
|
+
var path = require("path");
|
|
10
|
+
var fs = require("fs");
|
|
11
|
+
var os = require("os");
|
|
12
|
+
var http = require("http");
|
|
13
|
+
var execFileSync = require("child_process").execFileSync;
|
|
14
|
+
var WebSocket;
|
|
15
|
+
try { WebSocket = require("ws"); }
|
|
16
|
+
catch (e) {
|
|
17
|
+
// ws is bundled in packages/ae-panel/node_modules; resolve manually if normal require fails.
|
|
18
|
+
var alt = path.join(__dirname, "..", "node_modules", "ws");
|
|
19
|
+
WebSocket = require(alt);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var $status = document.getElementById("status");
|
|
23
|
+
var $port = document.getElementById("port");
|
|
24
|
+
var $ae = document.getElementById("ae");
|
|
25
|
+
var $jsx = document.getElementById("jsx");
|
|
26
|
+
var $reqs = document.getElementById("reqs");
|
|
27
|
+
var $log = document.getElementById("log");
|
|
28
|
+
function setStatus(text, klass) {
|
|
29
|
+
$status.textContent = text;
|
|
30
|
+
$status.className = klass || "";
|
|
31
|
+
}
|
|
32
|
+
var __reqCount = 0;
|
|
33
|
+
function bumpReq() { __reqCount += 1; $reqs.textContent = String(__reqCount); }
|
|
34
|
+
function log(level, msg) {
|
|
35
|
+
var d = document.createElement("div");
|
|
36
|
+
d.className = (level === "error" ? "err" : level === "warn" ? "warn" : "");
|
|
37
|
+
var ts = new Date().toISOString().substring(11, 19);
|
|
38
|
+
d.textContent = "[" + ts + "] " + msg;
|
|
39
|
+
$log.insertBefore(d, $log.firstChild);
|
|
40
|
+
// Trim
|
|
41
|
+
while ($log.childNodes.length > 80) $log.removeChild($log.lastChild);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var cs = new CSInterface();
|
|
45
|
+
var extDir = cs.getSystemPath(SystemPath.EXTENSION);
|
|
46
|
+
var bundlePath = path.join(extDir, "jsx", "bundle.jsx");
|
|
47
|
+
|
|
48
|
+
// ---------- ExtendScript evalScript: serialized via Promise chain ----------
|
|
49
|
+
var __evalChain = Promise.resolve();
|
|
50
|
+
function evalScript(script) {
|
|
51
|
+
var p = __evalChain.then(function () {
|
|
52
|
+
return new Promise(function (resolve, reject) {
|
|
53
|
+
cs.evalScript(script, function (raw) {
|
|
54
|
+
if (raw === "EvalScript error.") return reject(new Error("ExtendScript eval failed (syntax or runtime)"));
|
|
55
|
+
resolve(raw);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
// Keep the chain alive even on rejection so subsequent calls aren't blocked.
|
|
60
|
+
__evalChain = p.catch(function () { return null; });
|
|
61
|
+
return p;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Run an op through the dispatch table. Returns parsed JS object.
|
|
65
|
+
function runOp(op, args) {
|
|
66
|
+
var payload = JSON.stringify({ op: op, args: args || {} });
|
|
67
|
+
// Use \uXXXX-safe JSON pass: JSON.stringify on the panel side, JSON.parse on ExtendScript side.
|
|
68
|
+
var script = "JSON.stringify(dispatch(" + JSON.stringify(payload) + "))";
|
|
69
|
+
return evalScript(script).then(function (raw) {
|
|
70
|
+
if (raw === "undefined" || raw === undefined || raw === null) {
|
|
71
|
+
throw new Error("ExtendScript returned undefined for op " + op);
|
|
72
|
+
}
|
|
73
|
+
var parsed;
|
|
74
|
+
try { parsed = JSON.parse(raw); }
|
|
75
|
+
catch (e) { throw new Error("Bad JSON from ExtendScript for op " + op + ": " + String(raw).slice(0, 200)); }
|
|
76
|
+
if (!parsed.ok) {
|
|
77
|
+
var msg = parsed.error || "ExtendScript op failed";
|
|
78
|
+
var err = new Error(msg);
|
|
79
|
+
err.aeStack = parsed.stack;
|
|
80
|
+
err.aeLine = parsed.line;
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
return parsed.result;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------- Load bundle.jsx ----------
|
|
88
|
+
function loadJsxBundle() {
|
|
89
|
+
setStatus("Loading bundle.jsx…");
|
|
90
|
+
if (!fs.existsSync(bundlePath)) {
|
|
91
|
+
setStatus("bundle.jsx missing", "err");
|
|
92
|
+
log("error", "bundle.jsx not found at " + bundlePath + " — run `npm run build:jsx`");
|
|
93
|
+
throw new Error("bundle.jsx missing");
|
|
94
|
+
}
|
|
95
|
+
// Use $.evalFile from ExtendScript side so paths work cross-platform.
|
|
96
|
+
var loadScript = "$.evalFile(" + JSON.stringify(bundlePath) + "); typeof dispatch === 'function' ? 'ok' : 'no-dispatch';";
|
|
97
|
+
return new Promise(function (resolve, reject) {
|
|
98
|
+
cs.evalScript(loadScript, function (raw) {
|
|
99
|
+
if (raw === "ok") { $jsx.textContent = "loaded"; $jsx.className = "ok"; resolve(); }
|
|
100
|
+
else { $jsx.textContent = "fail: " + raw; $jsx.className = "err"; reject(new Error("dispatch not defined after loading bundle.jsx: " + raw)); }
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------- Port file (so MCP server can discover) ----------
|
|
106
|
+
function writePortFile(port) {
|
|
107
|
+
var dir = path.join(os.homedir(), ".engineroom-ae-mcp");
|
|
108
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch (e) {}
|
|
109
|
+
try { fs.writeFileSync(path.join(dir, "port"), String(port), "utf8"); } catch (e) { log("warn", "could not write port file: " + e.message); }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------- WS broadcast ----------
|
|
113
|
+
var wsClients = new Set();
|
|
114
|
+
function broadcast(eventObj) {
|
|
115
|
+
var str = JSON.stringify(eventObj);
|
|
116
|
+
wsClients.forEach(function (c) {
|
|
117
|
+
try { if (c.readyState === 1) c.send(str); } catch (e) {}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------- Vision: read PNG and base64-encode (for screenshot_* ops) ----------
|
|
122
|
+
function waitForPngFile(file, maxMs) {
|
|
123
|
+
var deadline = Date.now() + (maxMs || 3000);
|
|
124
|
+
return new Promise(function (resolve, reject) {
|
|
125
|
+
(function poll() {
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(file)) {
|
|
128
|
+
var stat = fs.statSync(file);
|
|
129
|
+
// Reasonable size + ensure not currently being written (re-check)
|
|
130
|
+
if (stat.size > 64) {
|
|
131
|
+
setTimeout(function () {
|
|
132
|
+
try {
|
|
133
|
+
var stat2 = fs.statSync(file);
|
|
134
|
+
if (stat2.size === stat.size) return resolve(stat2.size);
|
|
135
|
+
} catch (e) {}
|
|
136
|
+
if (Date.now() > deadline) return reject(new Error("PNG file write timed out"));
|
|
137
|
+
poll();
|
|
138
|
+
}, 30);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {}
|
|
143
|
+
if (Date.now() > deadline) return reject(new Error("PNG file did not appear: " + file));
|
|
144
|
+
setTimeout(poll, 40);
|
|
145
|
+
})();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Shrink the PNG in place with `sips` (macOS built-in, no npm dependency).
|
|
149
|
+
// saveFrameToPng always writes at full comp resolution, so this is the only
|
|
150
|
+
// place `downsample` can be honoured. Returns the dimensions actually
|
|
151
|
+
// produced — never claims a resize that did not happen.
|
|
152
|
+
function shrinkPng(file, width, height, factor) {
|
|
153
|
+
var outW = Math.max(1, Math.round(width / factor));
|
|
154
|
+
var outH = Math.max(1, Math.round(height / factor));
|
|
155
|
+
try {
|
|
156
|
+
// sips takes height then width.
|
|
157
|
+
execFileSync("sips", ["-z", String(outH), String(outW), file], { stdio: "ignore" });
|
|
158
|
+
return { width: outW, height: outH, downsample: factor };
|
|
159
|
+
} catch (e) {
|
|
160
|
+
log("warn", "downsample " + factor + "x failed, returning full resolution: " + e.message);
|
|
161
|
+
return {
|
|
162
|
+
width: width,
|
|
163
|
+
height: height,
|
|
164
|
+
downsample: 1,
|
|
165
|
+
warning: "downsample=" + factor + " was requested but `sips` failed (" + e.message +
|
|
166
|
+
"); returning the full-resolution frame instead.",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function readPngAsBase64(file, downsample, width, height) {
|
|
172
|
+
return waitForPngFile(file, 5000).then(function () {
|
|
173
|
+
// Resize only after the file has finished being written.
|
|
174
|
+
var dims = (downsample && downsample > 1)
|
|
175
|
+
? shrinkPng(file, width, height, downsample)
|
|
176
|
+
: { width: width, height: height, downsample: 1 };
|
|
177
|
+
var buf = fs.readFileSync(file);
|
|
178
|
+
var b64 = buf.toString("base64");
|
|
179
|
+
try { fs.unlinkSync(file); } catch (e) {}
|
|
180
|
+
return {
|
|
181
|
+
base64: b64,
|
|
182
|
+
bytes: buf.length,
|
|
183
|
+
width: dims.width,
|
|
184
|
+
height: dims.height,
|
|
185
|
+
downsample: dims.downsample,
|
|
186
|
+
warning: dims.warning,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------- Long job continuation loop ----------
|
|
192
|
+
function driveJob(jobId, progressToken) {
|
|
193
|
+
var totalGuess = null;
|
|
194
|
+
function step() {
|
|
195
|
+
return runOp("_continue_job", { jobId: jobId, chunkSize: 25 }).then(function (res) {
|
|
196
|
+
if (res.total) totalGuess = res.total;
|
|
197
|
+
if (!res.done) {
|
|
198
|
+
broadcast({ type: "progress", jobId: jobId, progress: res.progress, total: res.total, message: "running" });
|
|
199
|
+
// Yield to UI so AE stays responsive.
|
|
200
|
+
return new Promise(function (r) { setTimeout(r, 0); }).then(step);
|
|
201
|
+
}
|
|
202
|
+
if (res.failed) {
|
|
203
|
+
broadcast({ type: "error", jobId: jobId, error: res.error || "batch failed" });
|
|
204
|
+
return { done: true, failed: true, jobId: jobId, error: res.error, results: res.results, errors: res.errors, atIndex: res.atIndex };
|
|
205
|
+
}
|
|
206
|
+
broadcast({ type: "complete", jobId: jobId, result: { results: res.results, errors: res.errors, total: res.total || totalGuess, cancelled: !!res.cancelled } });
|
|
207
|
+
return { done: true, jobId: jobId, results: res.results, errors: res.errors, total: res.total || totalGuess, cancelled: !!res.cancelled };
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return step();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------- Op handler with vision/job specialization ----------
|
|
214
|
+
function handleOp(op, args, progressToken) {
|
|
215
|
+
// Vision ops: run JSX, then read PNG and base64-encode on Node side.
|
|
216
|
+
if (op === "screenshot_frame" || op === "screenshot_layer") {
|
|
217
|
+
var factor = (args && args.downsample) ? Math.round(args.downsample) : 1;
|
|
218
|
+
return runOp(op, args).then(function (info) {
|
|
219
|
+
return readPngAsBase64(info.path, factor, info.width, info.height).then(function (img) {
|
|
220
|
+
var out = {
|
|
221
|
+
width: img.width,
|
|
222
|
+
height: img.height,
|
|
223
|
+
fullWidth: info.width,
|
|
224
|
+
fullHeight: info.height,
|
|
225
|
+
downsample: img.downsample,
|
|
226
|
+
time: info.time,
|
|
227
|
+
compId: info.compId,
|
|
228
|
+
layerId: info.layerId,
|
|
229
|
+
mimeType: "image/png",
|
|
230
|
+
base64: img.base64,
|
|
231
|
+
bytes: img.bytes,
|
|
232
|
+
};
|
|
233
|
+
if (img.warning) out.warning = img.warning;
|
|
234
|
+
return out;
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// Long batches: returns {jobId, async, total}. We kick off background drive
|
|
239
|
+
// and return the jobId immediately.
|
|
240
|
+
if (op === "run_batch") {
|
|
241
|
+
return runOp(op, args).then(function (res) {
|
|
242
|
+
if (res && res.async && res.jobId) {
|
|
243
|
+
driveJob(res.jobId, progressToken).catch(function (e) {
|
|
244
|
+
broadcast({ type: "error", jobId: res.jobId, error: e.message });
|
|
245
|
+
});
|
|
246
|
+
return { jobId: res.jobId, async: true, total: res.total };
|
|
247
|
+
}
|
|
248
|
+
return res; // small batch: inline result already
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return runOp(op, args);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------- HTTP server ----------
|
|
255
|
+
function startHttp(port) {
|
|
256
|
+
return new Promise(function (resolve, reject) {
|
|
257
|
+
var server = http.createServer(function (req, res) {
|
|
258
|
+
var url = req.url || "/";
|
|
259
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
260
|
+
res.setHeader("Access-Control-Allow-Headers", "content-type");
|
|
261
|
+
|
|
262
|
+
if (url === "/health") {
|
|
263
|
+
res.statusCode = 200;
|
|
264
|
+
res.setHeader("content-type", "application/json");
|
|
265
|
+
res.end(JSON.stringify({ ok: true, port: port, bundleLoaded: true, ts: Date.now() }));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (req.method !== "POST") {
|
|
270
|
+
res.statusCode = 405; res.end("POST only");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
var bufs = [];
|
|
275
|
+
req.on("data", function (c) { bufs.push(c); });
|
|
276
|
+
req.on("end", function () {
|
|
277
|
+
var body;
|
|
278
|
+
try { body = JSON.parse(Buffer.concat(bufs).toString("utf8") || "{}"); }
|
|
279
|
+
catch (e) {
|
|
280
|
+
res.statusCode = 400; res.end(JSON.stringify({ ok: false, error: "Bad JSON: " + e.message }));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (url === "/op") {
|
|
285
|
+
bumpReq();
|
|
286
|
+
handleOp(body.op, body.args, body.progressToken)
|
|
287
|
+
.then(function (result) {
|
|
288
|
+
res.statusCode = 200; res.setHeader("content-type", "application/json");
|
|
289
|
+
res.end(JSON.stringify({ ok: true, result: result }));
|
|
290
|
+
})
|
|
291
|
+
.catch(function (err) {
|
|
292
|
+
log("error", body.op + ": " + err.message);
|
|
293
|
+
res.statusCode = 500; res.setHeader("content-type", "application/json");
|
|
294
|
+
res.end(JSON.stringify({ ok: false, error: err.message, stack: err.aeStack, line: err.aeLine }));
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (url === "/cancel") {
|
|
300
|
+
runOp("_cancel_job", { jobId: body.jobId })
|
|
301
|
+
.then(function (r) {
|
|
302
|
+
res.statusCode = 200; res.setHeader("content-type", "application/json");
|
|
303
|
+
res.end(JSON.stringify({ ok: true, result: r }));
|
|
304
|
+
})
|
|
305
|
+
.catch(function (e) {
|
|
306
|
+
res.statusCode = 500; res.setHeader("content-type", "application/json");
|
|
307
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (url === "/reload-jsx") {
|
|
313
|
+
// Dev-only: re-evalFile the bundle. Useful while editing .jsx without restarting AE.
|
|
314
|
+
loadJsxBundle()
|
|
315
|
+
.then(function () { res.statusCode = 200; res.end(JSON.stringify({ ok: true })); })
|
|
316
|
+
.catch(function (e) { res.statusCode = 500; res.end(JSON.stringify({ ok: false, error: e.message })); });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
res.statusCode = 404; res.end("not found");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
server.on("error", function (e) { reject(e); });
|
|
325
|
+
server.listen(port, "127.0.0.1", function () { resolve(server); });
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function startServers(startPort) {
|
|
330
|
+
var tries = 0;
|
|
331
|
+
function attempt(p) {
|
|
332
|
+
tries++;
|
|
333
|
+
return startHttp(p).then(function (server) {
|
|
334
|
+
// Mount WS on the same HTTP server.
|
|
335
|
+
var wss = new WebSocket.Server({ server: server, path: "/events" });
|
|
336
|
+
wss.on("connection", function (ws) {
|
|
337
|
+
wsClients.add(ws);
|
|
338
|
+
ws.on("close", function () { wsClients.delete(ws); });
|
|
339
|
+
});
|
|
340
|
+
return { port: p, server: server, wss: wss };
|
|
341
|
+
}).catch(function (e) {
|
|
342
|
+
if (e.code === "EADDRINUSE" && tries < 23 && p < 7799) {
|
|
343
|
+
return attempt(p + 1);
|
|
344
|
+
}
|
|
345
|
+
throw e;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return attempt(startPort);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------- Boot sequence ----------
|
|
352
|
+
setStatus("Starting…");
|
|
353
|
+
try {
|
|
354
|
+
var host = cs.getHostEnvironment();
|
|
355
|
+
$ae.textContent = (host && host.appName ? host.appName : "AE") + " " + (host && host.appVersion ? host.appVersion : "");
|
|
356
|
+
} catch (e) {}
|
|
357
|
+
|
|
358
|
+
loadJsxBundle()
|
|
359
|
+
.then(function () { return startServers(7777); })
|
|
360
|
+
.then(function (s) {
|
|
361
|
+
writePortFile(s.port);
|
|
362
|
+
$port.textContent = String(s.port);
|
|
363
|
+
$port.className = "ok";
|
|
364
|
+
setStatus("ready", "ok");
|
|
365
|
+
log("info", "Bridge listening on http://127.0.0.1:" + s.port);
|
|
366
|
+
})
|
|
367
|
+
.catch(function (e) {
|
|
368
|
+
setStatus("failed", "err");
|
|
369
|
+
log("error", e.message);
|
|
370
|
+
});
|
|
371
|
+
})();
|