@shotstack/shotstack-canvas 1.1.6 → 1.1.8
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/entry.node.cjs +132 -36
- package/dist/entry.node.cjs.map +1 -1
- package/dist/entry.node.d.cts +1 -0
- package/dist/entry.node.d.ts +1 -0
- package/dist/entry.node.js +132 -36
- package/dist/entry.node.js.map +1 -1
- package/dist/entry.web.js +57 -3
- package/dist/entry.web.js.map +1 -1
- package/package.json +2 -3
- package/public/hb.wasm +0 -0
package/dist/entry.node.d.cts
CHANGED
|
@@ -204,6 +204,7 @@ interface VideoGenerationOptions {
|
|
|
204
204
|
duration: number;
|
|
205
205
|
outputPath: string;
|
|
206
206
|
pixelRatio?: number;
|
|
207
|
+
hasAlpha?: boolean;
|
|
207
208
|
crf?: number;
|
|
208
209
|
preset?: "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow";
|
|
209
210
|
tune?: "film" | "animation" | "grain" | "stillimage" | "psnr" | "ssim" | "fastdecode" | "zerolatency";
|
package/dist/entry.node.d.ts
CHANGED
|
@@ -204,6 +204,7 @@ interface VideoGenerationOptions {
|
|
|
204
204
|
duration: number;
|
|
205
205
|
outputPath: string;
|
|
206
206
|
pixelRatio?: number;
|
|
207
|
+
hasAlpha?: boolean;
|
|
207
208
|
crf?: number;
|
|
208
209
|
preset?: "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow";
|
|
209
210
|
tune?: "film" | "animation" | "grain" | "stillimage" | "psnr" | "ssim" | "fastdecode" | "zerolatency";
|
package/dist/entry.node.js
CHANGED
|
@@ -126,9 +126,62 @@ var RichTextAssetSchema = Joi.object({
|
|
|
126
126
|
|
|
127
127
|
// src/wasm/hb-loader.ts
|
|
128
128
|
var hbSingleton = null;
|
|
129
|
+
function isNode() {
|
|
130
|
+
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
131
|
+
}
|
|
132
|
+
async function loadWasmWeb(wasmBaseURL) {
|
|
133
|
+
try {
|
|
134
|
+
if (wasmBaseURL) {
|
|
135
|
+
const url = wasmBaseURL.endsWith(".wasm") ? wasmBaseURL : wasmBaseURL.endsWith("/") ? `${wasmBaseURL}hb.wasm` : `${wasmBaseURL}/hb.wasm`;
|
|
136
|
+
console.log(`Fetching WASM from: ${url}`);
|
|
137
|
+
const response = await fetch(url);
|
|
138
|
+
if (response.ok) {
|
|
139
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
140
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
141
|
+
if (bytes.length >= 4 && bytes[0] === 0 && bytes[1] === 97 && bytes[2] === 115 && bytes[3] === 109) {
|
|
142
|
+
console.log(`\u2705 Valid WASM binary loaded (${bytes.length} bytes)`);
|
|
143
|
+
return arrayBuffer;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return void 0;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error("Error in loadWasmWeb:", err);
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function setupWasmFetchInterceptor(wasmBinary) {
|
|
154
|
+
const originalFetch = window.fetch;
|
|
155
|
+
window.fetch = function(input, init) {
|
|
156
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
157
|
+
if (url.includes("hb.wasm") || url.endsWith(".wasm")) {
|
|
158
|
+
console.log(`\u{1F504} Intercepted fetch for: ${url}`);
|
|
159
|
+
return Promise.resolve(
|
|
160
|
+
new Response(wasmBinary, {
|
|
161
|
+
status: 200,
|
|
162
|
+
statusText: "OK",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/wasm",
|
|
165
|
+
"Content-Length": wasmBinary.byteLength.toString()
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return originalFetch.apply(this, [input, init]);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
129
173
|
async function initHB(wasmBaseURL) {
|
|
130
174
|
if (hbSingleton) return hbSingleton;
|
|
131
175
|
try {
|
|
176
|
+
let wasmBinary;
|
|
177
|
+
wasmBinary = await loadWasmWeb(wasmBaseURL);
|
|
178
|
+
if (!wasmBinary) {
|
|
179
|
+
throw new Error("Failed to load WASM binary from any source");
|
|
180
|
+
}
|
|
181
|
+
console.log(`\u2705 WASM binary loaded successfully (${wasmBinary.byteLength} bytes)`);
|
|
182
|
+
if (!isNode()) {
|
|
183
|
+
setupWasmFetchInterceptor(wasmBinary);
|
|
184
|
+
}
|
|
132
185
|
const mod = await import("harfbuzzjs");
|
|
133
186
|
const candidate = mod.default;
|
|
134
187
|
let hb;
|
|
@@ -142,10 +195,11 @@ async function initHB(wasmBaseURL) {
|
|
|
142
195
|
if (!hb || typeof hb.createBuffer !== "function" || typeof hb.createFont !== "function") {
|
|
143
196
|
throw new Error("Failed to initialize HarfBuzz: unexpected export shape from 'harfbuzzjs'.");
|
|
144
197
|
}
|
|
145
|
-
void wasmBaseURL;
|
|
146
198
|
hbSingleton = hb;
|
|
199
|
+
console.log("\u2705 HarfBuzz initialized successfully");
|
|
147
200
|
return hbSingleton;
|
|
148
201
|
} catch (err) {
|
|
202
|
+
console.error("Failed to initialize HarfBuzz:", err);
|
|
149
203
|
throw new Error(
|
|
150
204
|
`Failed to initialize HarfBuzz: ${err instanceof Error ? err.message : String(err)}`
|
|
151
205
|
);
|
|
@@ -184,7 +238,7 @@ var FontRegistry = class _FontRegistry {
|
|
|
184
238
|
}
|
|
185
239
|
async _doInit() {
|
|
186
240
|
try {
|
|
187
|
-
this.hb = await initHB(
|
|
241
|
+
this.hb = await initHB("https://shotstack-ingest-api-dev-sources.s3.ap-southeast-2.amazonaws.com/euo5r93oyr/zzz01k9h-yycyx-2x2y6-qx9bj-7n567b/source.wasm");
|
|
188
242
|
} catch (error) {
|
|
189
243
|
this.initPromise = void 0;
|
|
190
244
|
throw error;
|
|
@@ -1176,6 +1230,7 @@ async function createNodePainter(opts) {
|
|
|
1176
1230
|
const hasBackground = !!(op.bg && op.bg.color);
|
|
1177
1231
|
needsAlphaExtraction = !hasBackground;
|
|
1178
1232
|
if (op.bg && op.bg.color) {
|
|
1233
|
+
ctx.clearRect(0, 0, op.width, op.height);
|
|
1179
1234
|
const { color, opacity, radius } = op.bg;
|
|
1180
1235
|
const c = parseHex6(color, opacity);
|
|
1181
1236
|
ctx.fillStyle = `rgba(${c.r},${c.g},${c.b},${c.a})`;
|
|
@@ -1189,10 +1244,15 @@ async function createNodePainter(opts) {
|
|
|
1189
1244
|
ctx.fillRect(0, 0, op.width, op.height);
|
|
1190
1245
|
}
|
|
1191
1246
|
} else {
|
|
1247
|
+
ctx.clearRect(0, 0, op.width, op.height);
|
|
1248
|
+
ctx.save();
|
|
1192
1249
|
ctx.fillStyle = "rgb(255, 255, 255)";
|
|
1193
1250
|
ctx.fillRect(0, 0, op.width, op.height);
|
|
1251
|
+
ctx.restore();
|
|
1252
|
+
offscreenCtx.save();
|
|
1194
1253
|
offscreenCtx.fillStyle = "rgb(0, 0, 0)";
|
|
1195
1254
|
offscreenCtx.fillRect(0, 0, op.width, op.height);
|
|
1255
|
+
offscreenCtx.restore();
|
|
1196
1256
|
}
|
|
1197
1257
|
continue;
|
|
1198
1258
|
}
|
|
@@ -1578,44 +1638,72 @@ var VideoGenerator = class {
|
|
|
1578
1638
|
tune = "animation",
|
|
1579
1639
|
profile = "high",
|
|
1580
1640
|
level = "4.2",
|
|
1581
|
-
pixFmt = "yuv420p"
|
|
1641
|
+
pixFmt = "yuv420p",
|
|
1642
|
+
hasAlpha = false
|
|
1582
1643
|
} = options;
|
|
1583
1644
|
const totalFrames = Math.max(2, Math.round(duration * fps) + 1);
|
|
1645
|
+
const finalOutputPath = hasAlpha ? outputPath.replace(/\.mp4$/, ".mov") : outputPath;
|
|
1584
1646
|
console.log(
|
|
1585
|
-
`\u{1F3AC} Generating video: ${width}x${height} @ ${fps}fps, ${duration}s (${totalFrames} frames)
|
|
1586
|
-
CRF=${crf}, preset=${preset}, tune=${tune}, profile=${profile}, level=${level}, pix_fmt=${pixFmt}`
|
|
1647
|
+
`\u{1F3AC} Generating video: ${width}x${height} @ ${fps}fps, ${duration}s (${totalFrames} frames)` + (hasAlpha ? " (MOV with alpha)" : "")
|
|
1587
1648
|
);
|
|
1588
1649
|
return new Promise(async (resolve, reject) => {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1650
|
+
let args;
|
|
1651
|
+
if (hasAlpha) {
|
|
1652
|
+
args = [
|
|
1653
|
+
"-y",
|
|
1654
|
+
"-f",
|
|
1655
|
+
"image2pipe",
|
|
1656
|
+
"-vcodec",
|
|
1657
|
+
"png",
|
|
1658
|
+
"-framerate",
|
|
1659
|
+
String(fps),
|
|
1660
|
+
"-i",
|
|
1661
|
+
"-",
|
|
1662
|
+
"-c:v",
|
|
1663
|
+
"prores_ks",
|
|
1664
|
+
"-profile:v",
|
|
1665
|
+
"4444",
|
|
1666
|
+
"-pix_fmt",
|
|
1667
|
+
"yuva444p10le",
|
|
1668
|
+
"-vendor",
|
|
1669
|
+
"apl0",
|
|
1670
|
+
"-r",
|
|
1671
|
+
String(fps),
|
|
1672
|
+
finalOutputPath
|
|
1673
|
+
];
|
|
1674
|
+
console.log(` Output: ${finalOutputPath} (ProRes 4444 with alpha)`);
|
|
1675
|
+
} else {
|
|
1676
|
+
args = [
|
|
1677
|
+
"-y",
|
|
1678
|
+
"-f",
|
|
1679
|
+
"image2pipe",
|
|
1680
|
+
"-vcodec",
|
|
1681
|
+
"png",
|
|
1682
|
+
"-framerate",
|
|
1683
|
+
String(fps),
|
|
1684
|
+
"-i",
|
|
1685
|
+
"-",
|
|
1686
|
+
"-c:v",
|
|
1687
|
+
"libx264",
|
|
1688
|
+
"-preset",
|
|
1689
|
+
preset,
|
|
1690
|
+
"-crf",
|
|
1691
|
+
String(crf),
|
|
1692
|
+
"-tune",
|
|
1693
|
+
tune,
|
|
1694
|
+
"-profile:v",
|
|
1695
|
+
profile,
|
|
1696
|
+
"-level",
|
|
1697
|
+
level,
|
|
1698
|
+
"-pix_fmt",
|
|
1699
|
+
pixFmt,
|
|
1700
|
+
"-r",
|
|
1701
|
+
String(fps),
|
|
1702
|
+
"-movflags",
|
|
1703
|
+
"+faststart",
|
|
1704
|
+
outputPath
|
|
1705
|
+
];
|
|
1706
|
+
}
|
|
1619
1707
|
const ffmpeg = spawn(this.ffmpegPath, args, { stdio: ["pipe", "inherit", "pipe"] });
|
|
1620
1708
|
let ffmpegError = "";
|
|
1621
1709
|
ffmpeg.stderr.on("data", (data) => {
|
|
@@ -1864,13 +1952,21 @@ async function createTextEngine(opts = {}) {
|
|
|
1864
1952
|
},
|
|
1865
1953
|
async generateVideo(asset, options) {
|
|
1866
1954
|
try {
|
|
1955
|
+
const hasBackground = !!asset.background?.color;
|
|
1956
|
+
const hasAnimation = !!asset.animation?.preset;
|
|
1957
|
+
const needsAlpha = !hasBackground && hasAnimation;
|
|
1958
|
+
console.log(
|
|
1959
|
+
`\u{1F3A8} Video settings: Animation=${hasAnimation}, Background=${hasBackground}, Alpha=${needsAlpha}`
|
|
1960
|
+
);
|
|
1867
1961
|
const finalOptions = {
|
|
1868
1962
|
width: asset.width ?? width,
|
|
1869
1963
|
height: asset.height ?? height,
|
|
1870
1964
|
fps,
|
|
1871
1965
|
duration: asset.animation?.duration ?? 3,
|
|
1872
1966
|
outputPath: options.outputPath ?? "output.mp4",
|
|
1873
|
-
pixelRatio: asset.pixelRatio ?? pixelRatio
|
|
1967
|
+
pixelRatio: asset.pixelRatio ?? pixelRatio,
|
|
1968
|
+
hasAlpha: needsAlpha,
|
|
1969
|
+
...options
|
|
1874
1970
|
};
|
|
1875
1971
|
const frameGenerator = async (time) => {
|
|
1876
1972
|
return this.renderFrame(asset, time);
|