@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.
@@ -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";
@@ -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";
@@ -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(this.wasmBaseURL);
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
- const args = [
1590
- "-y",
1591
- "-f",
1592
- "image2pipe",
1593
- "-vcodec",
1594
- "png",
1595
- "-framerate",
1596
- String(fps),
1597
- "-i",
1598
- "-",
1599
- "-c:v",
1600
- "libx264",
1601
- "-preset",
1602
- preset,
1603
- "-crf",
1604
- String(crf),
1605
- "-tune",
1606
- tune,
1607
- "-profile:v",
1608
- profile,
1609
- "-level",
1610
- level,
1611
- "-pix_fmt",
1612
- pixFmt,
1613
- "-r",
1614
- String(fps),
1615
- "-movflags",
1616
- "+faststart",
1617
- outputPath
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);