@hyperframes/aws-lambda 0.8.185 → 0.8.188

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.
@@ -32,7 +32,7 @@ var MAX_SINGLE_PUT_BYTES = 5 * 1024 ** 3;
32
32
  var MAX_PARTS = 1e4;
33
33
  var MIN_FILE_PART_BYTES = 64 * 1024 * 1024;
34
34
  var FILE_PARTS_IN_FLIGHT = 8;
35
- var PART_ATTEMPTS = 3;
35
+ var UPLOAD_ATTEMPTS = 3;
36
36
  function parseS3Uri(uri) {
37
37
  if (!uri.startsWith("s3://")) {
38
38
  throw new Error(`[s3Transport] expected s3:// URI, got: ${JSON.stringify(uri)}`);
@@ -111,14 +111,18 @@ async function uploadFileToS3(client, localPath, uri, contentType) {
111
111
  });
112
112
  return;
113
113
  }
114
- await client.send(
115
- new PutObjectCommand({
116
- Bucket: bucket,
117
- Key: key,
118
- Body: createReadStream(localPath),
119
- ContentType: contentType,
120
- ContentLength: size
121
- })
114
+ await sendFileBody(
115
+ localPath,
116
+ void 0,
117
+ (body) => client.send(
118
+ new PutObjectCommand({
119
+ Bucket: bucket,
120
+ Key: key,
121
+ Body: body,
122
+ ContentType: contentType,
123
+ ContentLength: size
124
+ })
125
+ )
122
126
  );
123
127
  }
124
128
  async function uploadContentAddressedFileToS3(client, localPath, uri, expectedSha256, contentType) {
@@ -152,9 +156,10 @@ async function putImmutableFile(client, localPath, uri, expectedSha256, contentT
152
156
  }
153
157
  }
154
158
  async function putImmutableBody(client, localPath, bucket, key, size, expectedSha256, contentType) {
155
- const body = createReadStream(localPath);
156
- try {
157
- await client.send(
159
+ await sendFileBody(
160
+ localPath,
161
+ void 0,
162
+ (body) => client.send(
158
163
  new PutObjectCommand({
159
164
  Bucket: bucket,
160
165
  Key: key,
@@ -165,10 +170,8 @@ async function putImmutableBody(client, localPath, bucket, key, size, expectedSh
165
170
  ChecksumSHA256: Buffer.from(expectedSha256, "hex").toString("base64"),
166
171
  IfNoneMatch: "*"
167
172
  })
168
- );
169
- } finally {
170
- body.destroy();
171
- }
173
+ )
174
+ );
172
175
  }
173
176
  async function multipartUpload(client, create2, sendParts, complete = {}) {
174
177
  const created = await client.send(new CreateMultipartUploadCommand(create2));
@@ -255,22 +258,29 @@ async function hashRange(path, { start, end }, whole) {
255
258
  return part.digest("base64");
256
259
  }
257
260
  async function sendFilePart(client, target, partNumber, path, { start, end }, checksum) {
261
+ const result = await sendFileBody(
262
+ path,
263
+ { start, end },
264
+ (body) => client.send(
265
+ new UploadPartCommand({
266
+ ...target,
267
+ PartNumber: partNumber,
268
+ Body: body,
269
+ ContentLength: end - start,
270
+ ChecksumSHA256: checksum
271
+ })
272
+ )
273
+ );
274
+ if (!result.ETag) throw new Error(`[s3Transport] part ${partNumber} returned no ETag`);
275
+ return { ETag: result.ETag, PartNumber: partNumber, ChecksumSHA256: checksum };
276
+ }
277
+ async function sendFileBody(path, range, send) {
258
278
  for (let attempt = 1; ; attempt++) {
259
- const body = createReadStream(path, { start, end: end - 1 });
279
+ const body = createReadStream(path, range && { start: range.start, end: range.end - 1 });
260
280
  try {
261
- const result = await client.send(
262
- new UploadPartCommand({
263
- ...target,
264
- PartNumber: partNumber,
265
- Body: body,
266
- ContentLength: end - start,
267
- ChecksumSHA256: checksum
268
- })
269
- );
270
- if (!result.ETag) throw new Error(`[s3Transport] part ${partNumber} returned no ETag`);
271
- return { ETag: result.ETag, PartNumber: partNumber, ChecksumSHA256: checksum };
281
+ return await send(body);
272
282
  } catch (error) {
273
- if (attempt >= PART_ATTEMPTS) throw error;
283
+ if (attempt >= UPLOAD_ATTEMPTS || !isTransientS3Error(error)) throw error;
274
284
  } finally {
275
285
  body.destroy();
276
286
  }
@@ -420,6 +430,13 @@ function isS3PreconditionFailed(error) {
420
430
  const metadata = isRecord(error.$metadata) ? error.$metadata : void 0;
421
431
  return error.name === "PreconditionFailed" || metadata?.httpStatusCode === 412;
422
432
  }
433
+ var TRANSIENT_S3_ERRORS = /* @__PURE__ */ new Set(["TimeoutError", "RequestTimeout", "RequestTimeoutException"]);
434
+ function isTransientS3Error(error) {
435
+ if (!isRecord(error)) return false;
436
+ if (typeof error.name === "string" && TRANSIENT_S3_ERRORS.has(error.name)) return true;
437
+ const status = isRecord(error.$metadata) ? error.$metadata.httpStatusCode : void 0;
438
+ return typeof status !== "number" || status === 429 || status >= 500;
439
+ }
423
440
  function isRecord(value) {
424
441
  return value !== null && typeof value === "object" && !Array.isArray(value);
425
442
  }
@@ -16,7 +16,7 @@ import {
16
16
  siteManifestUri,
17
17
  siteTarballUri,
18
18
  uploadContentAddressedFileToS3
19
- } from "./chunk-SGNVVPIS.js";
19
+ } from "./chunk-CKQ6SAJX.js";
20
20
  import {
21
21
  CONTENT_STORE_RETENTION_DAYS
22
22
  } from "./chunk-YS3KY3C5.js";
@@ -234,14 +234,14 @@ async function assertStackReadsSites(opts) {
234
234
  }
235
235
 
236
236
  // src/sdk/costAccounting.ts
237
- var LAMBDA_USD_PER_GB_SECOND = 166667e-10;
237
+ var LAMBDA_USD_PER_GB_SECOND = { x86_64: 166667e-10, arm64: 133334e-10 };
238
238
  var SFN_USD_PER_TRANSITION = 25e-6;
239
239
  function computeRenderCost(lambdaInvocations, stateTransitions) {
240
240
  let lambdaUsd = 0;
241
241
  let anyEstimated = false;
242
242
  for (const inv of lambdaInvocations) {
243
243
  const gbSeconds = inv.memorySizeMb / 1024 * (inv.billedDurationMs / 1e3);
244
- lambdaUsd += gbSeconds * LAMBDA_USD_PER_GB_SECOND;
244
+ lambdaUsd += gbSeconds * LAMBDA_USD_PER_GB_SECOND[inv.architecture ?? "x86_64"];
245
245
  if (inv.estimated) anyEstimated = true;
246
246
  }
247
247
  const stepFunctionsUsd = stateTransitions * SFN_USD_PER_TRANSITION;
@@ -289,10 +289,10 @@ async function getRenderProgress(opts) {
289
289
  const startedAt = describe.startDate?.toISOString() ?? (/* @__PURE__ */ new Date(0)).toISOString();
290
290
  const endedAt = describe.stopDate?.toISOString() ?? null;
291
291
  const history = await loadFullHistory(sfn, opts.executionArn);
292
- const summary = summarizeHistory(history, memoryMb);
292
+ const summary = summarizeHistory(history, memoryMb, opts.videoArchitecture);
293
293
  const mapRun = findChunkMapRun(history);
294
294
  const warnings = [];
295
- if (mapRun?.exited) addChunkResults(summary, mapRun.output, memoryMb);
295
+ if (mapRun?.exited) addChunkResults(summary, mapRun.output, memoryMb, opts.videoArchitecture);
296
296
  else if (mapRun) {
297
297
  await addRunningMapRun(sfn, mapRun.arn, summary, memoryMb).catch((err) => {
298
298
  if (err?.name !== "AccessDeniedException") throw err;
@@ -353,17 +353,12 @@ function findChunkMapRun(events) {
353
353
  }
354
354
  return run;
355
355
  }
356
- function addChunkResults(summary, mapOutput, memoryMb) {
356
+ function addChunkResults(summary, mapOutput, memoryMb, architecture) {
357
357
  const chunks = parseJson(mapOutput)?.Chunks;
358
358
  for (const chunk of Array.isArray(chunks) ? chunks : []) {
359
359
  summary.lambdasInvoked++;
360
360
  summary.stateTransitions++;
361
- const billedDurationMs = inferBilledMs(chunk);
362
- summary.lambdaInvocations.push({
363
- billedDurationMs,
364
- memorySizeMb: memoryMb,
365
- estimated: billedDurationMs === 0
366
- });
361
+ summary.lambdaInvocations.push(billedInvocation(chunk, memoryMb, architecture));
367
362
  applyPayloadFrameCounts(chunk, "RenderChunk", (delta) => {
368
363
  summary.framesRendered += delta;
369
364
  });
@@ -395,7 +390,7 @@ async function failedChunkErrors(sfn, mapRunArn, memoryMb) {
395
390
  }
396
391
  return errors;
397
392
  }
398
- function summarizeHistory(events, memoryMb) {
393
+ function summarizeHistory(events, memoryMb, videoArchitecture) {
399
394
  let framesRendered = 0;
400
395
  let totalFrames = null;
401
396
  let lambdasInvoked = 0;
@@ -432,12 +427,13 @@ function summarizeHistory(events, memoryMb) {
432
427
  if (ev.taskSucceededEventDetails?.resourceType !== "lambda") break;
433
428
  const wrapped = parseJson(ev.taskSucceededEventDetails?.output);
434
429
  const payload = unwrapLambdaPayload(wrapped);
435
- const billedDurationMs = inferBilledMs(payload);
436
- lambdaInvocations.push({
437
- billedDurationMs,
438
- memorySizeMb: memoryMb,
439
- estimated: billedDurationMs === 0
440
- });
430
+ lambdaInvocations.push(
431
+ billedInvocation(
432
+ payload,
433
+ memoryMb,
434
+ stateArchitecture(currentLambdaState, videoArchitecture)
435
+ )
436
+ );
441
437
  applyPayloadFrameCounts(payload, currentLambdaState, (delta) => {
442
438
  framesRendered += delta;
443
439
  });
@@ -449,12 +445,13 @@ function summarizeHistory(events, memoryMb) {
449
445
  }
450
446
  case "LambdaFunctionSucceeded": {
451
447
  const payload = parseJson(ev.lambdaFunctionSucceededEventDetails?.output);
452
- const billedDurationMs = inferBilledMs(payload);
453
- lambdaInvocations.push({
454
- billedDurationMs,
455
- memorySizeMb: memoryMb,
456
- estimated: billedDurationMs === 0
457
- });
448
+ lambdaInvocations.push(
449
+ billedInvocation(
450
+ payload,
451
+ memoryMb,
452
+ stateArchitecture(currentLambdaState, videoArchitecture)
453
+ )
454
+ );
458
455
  applyPayloadFrameCounts(payload, currentLambdaState, (delta) => {
459
456
  framesRendered += delta;
460
457
  });
@@ -559,6 +556,18 @@ function isRenderChunksMapState(name) {
559
556
  function isAssembleState(name) {
560
557
  return name === "Assemble" || name === "AssembleV2";
561
558
  }
559
+ function billedInvocation(payload, memoryMb, architecture) {
560
+ const billedDurationMs = inferBilledMs(payload);
561
+ return {
562
+ billedDurationMs,
563
+ memorySizeMb: memoryMb,
564
+ estimated: billedDurationMs === 0,
565
+ architecture
566
+ };
567
+ }
568
+ function stateArchitecture(state, videoArchitecture) {
569
+ return state === "Plan" || state === "PlanV2" || isRenderChunkState(state) ? videoArchitecture : void 0;
570
+ }
562
571
  function inferBilledMs(payload) {
563
572
  if (!payload || typeof payload !== "object") return 0;
564
573
  const obj = payload;
@@ -15,7 +15,7 @@ import {
15
15
  untarDirectory,
16
16
  uploadContentAddressedFileToS3,
17
17
  uploadFileToS3
18
- } from "./chunk-SGNVVPIS.js";
18
+ } from "./chunk-CKQ6SAJX.js";
19
19
  import {
20
20
  CONTENT_STORE_PREFIX
21
21
  } from "./chunk-YS3KY3C5.js";
@@ -844,7 +844,11 @@ async function handleAssembleV2(event, deps) {
844
844
  if (chunksAreStitchedMp4(planDir, event.Cfr === true)) {
845
845
  return await stitchChunksInS3(s3, event, manifest, started);
846
846
  }
847
- const audioPath = resolvePlanAudioPath(planDir);
847
+ let audioPath = resolvePlanAudioPath(planDir);
848
+ if (event.AudioS3Uri !== null) {
849
+ audioPath = join2(planDir, PLAN_AUDIO_RELATIVE_PATH);
850
+ await downloadS3ObjectToFile(s3, event.AudioS3Uri, audioPath);
851
+ }
848
852
  const chunksStarted = Date.now();
849
853
  const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
850
854
  const chunksMs = Date.now() - chunksStarted;
package/dist/handler.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  handler,
3
3
  unwrapEvent
4
- } from "./_chunks/chunk-QCYPNODV.js";
5
- import "./_chunks/chunk-SGNVVPIS.js";
4
+ } from "./_chunks/chunk-ROTCAPRH.js";
5
+ import "./_chunks/chunk-CKQ6SAJX.js";
6
6
  import "./_chunks/chunk-YS3KY3C5.js";
7
7
  export {
8
8
  handler,
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  resolveChromeExecutablePath,
7
7
  resolveChromeSource,
8
8
  unwrapEvent
9
- } from "./_chunks/chunk-QCYPNODV.js";
9
+ } from "./_chunks/chunk-ROTCAPRH.js";
10
10
  import {
11
11
  InvalidConfigError,
12
12
  computeRenderCost,
@@ -14,7 +14,7 @@ import {
14
14
  getRenderProgress,
15
15
  renderToLambda,
16
16
  validateDistributedRenderConfig
17
- } from "./_chunks/chunk-5FLGGUWE.js";
17
+ } from "./_chunks/chunk-EG2WRNUL.js";
18
18
  import {
19
19
  downloadS3ObjectToFile,
20
20
  downloadS3ObjectToFileVerified,
@@ -24,7 +24,7 @@ import {
24
24
  untarDirectory,
25
25
  uploadContentAddressedFileToS3,
26
26
  uploadFileToS3
27
- } from "./_chunks/chunk-SGNVVPIS.js";
27
+ } from "./_chunks/chunk-CKQ6SAJX.js";
28
28
  import "./_chunks/chunk-YS3KY3C5.js";
29
29
  export {
30
30
  ChromeBinaryUnavailableError,
@@ -16,6 +16,12 @@
16
16
  * see the dollar value but downstream automation can also read the
17
17
  * raw number.
18
18
  */
19
+ /** On-demand Lambda price, us-east-1: USD per GB-second, by function architecture. */
20
+ declare const LAMBDA_USD_PER_GB_SECOND: {
21
+ readonly x86_64: 0.0000166667;
22
+ readonly arm64: 0.0000133334;
23
+ };
24
+ export type LambdaArchitecture = keyof typeof LAMBDA_USD_PER_GB_SECOND;
19
25
  /** Raw history event subset the cost calc cares about. Caller filters from `getExecutionHistory`. */
20
26
  export interface BilledLambdaInvocation {
21
27
  /** Millis of Lambda billed duration. Carried on `TaskSucceeded`/`TaskFailed` events. */
@@ -24,6 +30,8 @@ export interface BilledLambdaInvocation {
24
30
  memorySizeMb: number;
25
31
  /** `true` if the event payload did NOT carry a billed duration and we fell back to `Duration` or a constant. */
26
32
  estimated: boolean;
33
+ /** Architecture of the invoked function; x86_64 when omitted. */
34
+ architecture?: LambdaArchitecture;
27
35
  }
28
36
  /** Result of {@link computeRenderCost}. */
29
37
  export interface RenderCost {
@@ -48,3 +56,4 @@ export interface RenderCost {
48
56
  * workflows. Express workflows price differently and are out of scope.
49
57
  */
50
58
  export declare function computeRenderCost(lambdaInvocations: BilledLambdaInvocation[], stateTransitions: number): RenderCost;
59
+ export {};
@@ -20,7 +20,7 @@
20
20
  * "JSON.parse this string and grab two fields."
21
21
  */
22
22
  import { SFNClient, type SFNClientConfig } from "@aws-sdk/client-sfn";
23
- import { type RenderCost } from "./costAccounting.js";
23
+ import { type LambdaArchitecture, type RenderCost } from "./costAccounting.js";
24
24
  /** Options for {@link getRenderProgress}. */
25
25
  export interface GetRenderProgressOptions {
26
26
  /** Execution ARN from a {@link renderToLambda} call. */
@@ -31,6 +31,8 @@ export interface GetRenderProgressOptions {
31
31
  * `LambdaMemoryMb` parameter the stack was deployed with.
32
32
  */
33
33
  defaultMemorySizeMb?: number;
34
+ /** Architecture of the stack's video function, which runs the plan and the chunks: `arm64` when it has one. */
35
+ videoArchitecture?: LambdaArchitecture;
34
36
  region?: string;
35
37
  /** Test injection seam. */
36
38
  sfn?: SFNClient;
@@ -12,6 +12,7 @@ export { assertStackReadsSites, renderToLambda, type RenderHandle, type RenderTo
12
12
  export { getRenderProgress, type GetRenderProgressOptions, type RenderError, type RenderProgress, type RenderStatus, } from "./getRenderProgress.js";
13
13
  export { type BilledLambdaInvocation, computeRenderCost, type RenderCost, } from "./costAccounting.js";
14
14
  export { InvalidConfigError, MAX_STEP_FUNCTIONS_INPUT_BYTES, validateDistributedRenderConfig, validateStepFunctionsInputSize, validateVariablesPayload, } from "./validateConfig.js";
15
- export { type PrebuiltHandler, prebuiltHandler } from "./prebuiltHandler.js";
15
+ export { type PrebuiltHandler, prebuiltHandler, prebuiltVideoImage, type ReleaseAssetManifest, } from "./prebuiltHandler.js";
16
+ export { ensureEcrRepository, type EnsureEcrRepositoryOptions, type PushedImage, pushImageToEcr, type PushImageToEcrOptions, } from "./pushImageToEcr.js";
16
17
  export type { SerializableDistributedRenderConfig } from "../events.js";
17
18
  export type { DistributedFormat } from "../formatExtension.js";
package/dist/sdk/index.js CHANGED
@@ -10,8 +10,10 @@ import {
10
10
  validateDistributedRenderConfig,
11
11
  validateStepFunctionsInputSize,
12
12
  validateVariablesPayload
13
- } from "../_chunks/chunk-5FLGGUWE.js";
14
- import "../_chunks/chunk-SGNVVPIS.js";
13
+ } from "../_chunks/chunk-EG2WRNUL.js";
14
+ import {
15
+ sha256File
16
+ } from "../_chunks/chunk-CKQ6SAJX.js";
15
17
  import "../_chunks/chunk-YS3KY3C5.js";
16
18
 
17
19
  // src/sdk/prebuiltHandler.ts
@@ -20,6 +22,7 @@ import { dirname, join } from "node:path";
20
22
  import { fileURLToPath } from "node:url";
21
23
  var PREBUILT_DIR = "prebuilt";
22
24
  var HANDLER_MANIFEST_FILE = "handler-zip.json";
25
+ var VIDEO_IMAGE_MANIFEST_FILE = "video-image.json";
23
26
  function adapterPackageRoot() {
24
27
  const from = fileURLToPath(import.meta.url);
25
28
  for (let dir = dirname(from); dir !== dirname(dir); dir = dirname(dir)) {
@@ -29,12 +32,196 @@ function adapterPackageRoot() {
29
32
  }
30
33
  throw new Error(`[aws-lambda] no @hyperframes/aws-lambda package.json above ${from}`);
31
34
  }
35
+ function readManifest(root, file) {
36
+ const path = join(root, PREBUILT_DIR, file);
37
+ if (!existsSync(path)) return void 0;
38
+ return JSON.parse(readFileSync(path, "utf-8"));
39
+ }
32
40
  function prebuiltHandler(root = adapterPackageRoot()) {
33
- const dir = join(root, PREBUILT_DIR);
34
- const manifestPath = join(dir, HANDLER_MANIFEST_FILE);
35
- if (!existsSync(manifestPath)) return void 0;
36
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
37
- return { ...manifest, templatePath: join(dir, "template.yaml") };
41
+ const manifest = readManifest(root, HANDLER_MANIFEST_FILE);
42
+ return manifest && { ...manifest, templatePath: join(root, PREBUILT_DIR, "template.yaml") };
43
+ }
44
+ function prebuiltVideoImage(root = adapterPackageRoot()) {
45
+ return readManifest(root, VIDEO_IMAGE_MANIFEST_FILE);
46
+ }
47
+
48
+ // src/sdk/pushImageToEcr.ts
49
+ import { open, readFile, stat } from "node:fs/promises";
50
+ import { join as join2 } from "node:path";
51
+ import {
52
+ BatchCheckLayerAvailabilityCommand,
53
+ CompleteLayerUploadCommand,
54
+ CreateRepositoryCommand,
55
+ DescribeRepositoriesCommand,
56
+ ECRClient,
57
+ InitiateLayerUploadCommand,
58
+ ListTagsForResourceCommand,
59
+ PutImageCommand,
60
+ UploadLayerPartCommand
61
+ } from "@aws-sdk/client-ecr";
62
+ var MANIFEST_MEDIA_TYPES = /* @__PURE__ */ new Set([
63
+ "application/vnd.oci.image.manifest.v1+json",
64
+ "application/vnd.docker.distribution.manifest.v2+json"
65
+ ]);
66
+ var CHECK_BATCH_SIZE = 100;
67
+ async function ensureEcrRepository(opts) {
68
+ const { repositoryName, tags } = opts;
69
+ const ecr = opts.ecr ?? new ECRClient({ region: opts.region });
70
+ try {
71
+ const created = await ecr.send(new CreateRepositoryCommand({ repositoryName, tags }));
72
+ return { repositoryUri: repositoryUriOf(created.repository, repositoryName) };
73
+ } catch (error) {
74
+ if (error.name !== "RepositoryAlreadyExistsException") throw error;
75
+ }
76
+ const found = await ecr.send(
77
+ new DescribeRepositoriesCommand({ repositoryNames: [repositoryName] })
78
+ );
79
+ const repository = found.repositories?.[0];
80
+ if (tags?.length) {
81
+ const listed = await ecr.send(
82
+ new ListTagsForResourceCommand({ resourceArn: repository?.repositoryArn })
83
+ );
84
+ const has = new Map((listed.tags ?? []).map((tag) => [tag.Key, tag.Value]));
85
+ const missing = tags.filter((tag) => has.get(tag.Key) !== tag.Value);
86
+ if (missing.length > 0) {
87
+ const wanted = missing.map((tag) => `${tag.Key}=${tag.Value}`).join(", ");
88
+ throw new Error(
89
+ `[ensureEcrRepository] ${repositoryName} already exists without ${wanted}; refusing to adopt a repository this caller did not create`
90
+ );
91
+ }
92
+ }
93
+ return { repositoryUri: repositoryUriOf(repository, repositoryName) };
94
+ }
95
+ function repositoryUriOf(repository, repositoryName) {
96
+ if (!repository?.repositoryUri) {
97
+ throw new Error(`[ensureEcrRepository] ECR returned no repositoryUri for ${repositoryName}`);
98
+ }
99
+ return repository.repositoryUri;
100
+ }
101
+ async function pushImageToEcr(opts) {
102
+ const { layoutDir, repositoryName, log = () => {
103
+ } } = opts;
104
+ const ecr = opts.ecr ?? new ECRClient({ region: opts.region });
105
+ const manifestDescriptor = await readManifestDescriptor(layoutDir);
106
+ await verifyBlob(layoutDir, manifestDescriptor);
107
+ const manifestText = await readFile(blobPath(layoutDir, manifestDescriptor.digest), "utf-8");
108
+ const manifest = JSON.parse(manifestText);
109
+ if (!manifest.config || !Array.isArray(manifest.layers)) {
110
+ throw new Error(
111
+ `[pushImageToEcr] manifest ${manifestDescriptor.digest} has no config or layers`
112
+ );
113
+ }
114
+ const blobs = [manifest.config, ...manifest.layers];
115
+ for (const blob of blobs) await verifyBlob(layoutDir, blob);
116
+ const config = JSON.parse(
117
+ await readFile(blobPath(layoutDir, manifest.config.digest), "utf-8")
118
+ );
119
+ if (!config.architecture || !config.os) {
120
+ throw new Error(
121
+ `[pushImageToEcr] image config ${manifest.config.digest} names no architecture/os; build the image for one platform`
122
+ );
123
+ }
124
+ const missing = await unavailableDigests(ecr, repositoryName, blobs);
125
+ for (const blob of blobs) {
126
+ if (!missing.delete(blob.digest)) continue;
127
+ log(`uploading ${blob.digest} (${blob.size} bytes)`);
128
+ await uploadBlob(ecr, repositoryName, blobPath(layoutDir, blob.digest), blob);
129
+ }
130
+ try {
131
+ await ecr.send(
132
+ new PutImageCommand({
133
+ repositoryName,
134
+ imageManifest: manifestText,
135
+ imageManifestMediaType: manifestDescriptor.mediaType,
136
+ imageTag: opts.tag,
137
+ imageDigest: manifestDescriptor.digest
138
+ })
139
+ );
140
+ } catch (error) {
141
+ if (error.name !== "ImageAlreadyExistsException") throw error;
142
+ }
143
+ return {
144
+ imageUri: `${opts.repositoryUri}@${manifestDescriptor.digest}`,
145
+ digest: manifestDescriptor.digest,
146
+ architecture: config.architecture,
147
+ os: config.os
148
+ };
149
+ }
150
+ async function readManifestDescriptor(layoutDir) {
151
+ const index = JSON.parse(await readFile(join2(layoutDir, "index.json"), "utf-8"));
152
+ const manifests = index.manifests ?? [];
153
+ const [only] = manifests;
154
+ if (manifests.length !== 1 || !only || !MANIFEST_MEDIA_TYPES.has(only.mediaType)) {
155
+ const found = manifests.map((m) => m.mediaType).join(", ") || "none";
156
+ throw new Error(
157
+ `[pushImageToEcr] ${layoutDir}/index.json must list exactly one single-platform image manifest; found ${manifests.length} (${found}). Build one platform with --provenance=false --sbom=false.`
158
+ );
159
+ }
160
+ return only;
161
+ }
162
+ function blobPath(layoutDir, digest) {
163
+ const hex = /^sha256:([a-f0-9]{64})$/.exec(digest)?.[1];
164
+ if (!hex) throw new Error(`[pushImageToEcr] unsupported blob digest ${JSON.stringify(digest)}`);
165
+ return join2(layoutDir, "blobs", "sha256", hex);
166
+ }
167
+ async function verifyBlob(layoutDir, blob) {
168
+ const path = blobPath(layoutDir, blob.digest);
169
+ const size = (await stat(path)).size;
170
+ if (size !== blob.size || `sha256:${await sha256File(path)}` !== blob.digest) {
171
+ throw new Error(
172
+ `[pushImageToEcr] blob ${blob.digest} (${size} bytes, expected ${blob.size}) does not match its descriptor; the image layout is corrupt`
173
+ );
174
+ }
175
+ }
176
+ async function unavailableDigests(ecr, repositoryName, blobs) {
177
+ const missing = new Set(blobs.map((blob) => blob.digest));
178
+ const digests = [...missing];
179
+ for (let i = 0; i < digests.length; i += CHECK_BATCH_SIZE) {
180
+ const { layers = [] } = await ecr.send(
181
+ new BatchCheckLayerAvailabilityCommand({
182
+ repositoryName,
183
+ layerDigests: digests.slice(i, i + CHECK_BATCH_SIZE)
184
+ })
185
+ );
186
+ for (const layer of layers) {
187
+ if (layer.layerAvailability === "AVAILABLE" && layer.layerDigest) {
188
+ missing.delete(layer.layerDigest);
189
+ }
190
+ }
191
+ }
192
+ return missing;
193
+ }
194
+ async function uploadBlob(ecr, repositoryName, path, blob) {
195
+ const { uploadId, partSize } = await ecr.send(new InitiateLayerUploadCommand({ repositoryName }));
196
+ if (!uploadId || !partSize) {
197
+ throw new Error(`[pushImageToEcr] InitiateLayerUpload returned no uploadId/partSize`);
198
+ }
199
+ const file = await open(path);
200
+ try {
201
+ for (let first = 0; first < blob.size; first += partSize) {
202
+ const part = Buffer.alloc(Math.min(partSize, blob.size - first));
203
+ const { bytesRead } = await file.read(part, 0, part.length, first);
204
+ if (bytesRead !== part.length) throw new Error(`[pushImageToEcr] ${path} changed on disk`);
205
+ await ecr.send(
206
+ new UploadLayerPartCommand({
207
+ repositoryName,
208
+ uploadId,
209
+ partFirstByte: first,
210
+ partLastByte: first + part.length - 1,
211
+ layerPartBlob: part
212
+ })
213
+ );
214
+ }
215
+ } finally {
216
+ await file.close();
217
+ }
218
+ try {
219
+ await ecr.send(
220
+ new CompleteLayerUploadCommand({ repositoryName, uploadId, layerDigests: [blob.digest] })
221
+ );
222
+ } catch (error) {
223
+ if (error.name !== "LayerAlreadyExistsException") throw error;
224
+ }
38
225
  }
39
226
  export {
40
227
  InvalidConfigError,
@@ -42,8 +229,11 @@ export {
42
229
  assertStackReadsSites,
43
230
  computeRenderCost,
44
231
  deploySite,
232
+ ensureEcrRepository,
45
233
  getRenderProgress,
46
234
  prebuiltHandler,
235
+ prebuiltVideoImage,
236
+ pushImageToEcr,
47
237
  renderToLambda,
48
238
  siteHandleFromId,
49
239
  validateDistributedRenderConfig,
@@ -1,13 +1,15 @@
1
1
  export declare const HANDLER_ZIP_ASSET = "hyperframes-lambda-handler.zip";
2
2
  export declare const PREBUILT_DIR = "prebuilt";
3
3
  export declare const HANDLER_MANIFEST_FILE = "handler-zip.json";
4
- export interface HandlerZipManifest {
4
+ export declare const VIDEO_IMAGE_ASSET = "hyperframes-lambda-video-arm64.oci.tar";
5
+ export declare const VIDEO_IMAGE_MANIFEST_FILE = "video-image.json";
6
+ export interface ReleaseAssetManifest {
5
7
  version: string;
6
8
  url: string;
7
9
  sha256: string;
8
10
  bytes: number;
9
11
  }
10
- export interface PrebuiltHandler extends HandlerZipManifest {
12
+ export interface PrebuiltHandler extends ReleaseAssetManifest {
11
13
  /** SAM template whose `CodeUri` is `handler.zip` in the same directory. */
12
14
  templatePath: string;
13
15
  }
@@ -15,3 +17,5 @@ export interface PrebuiltHandler extends HandlerZipManifest {
15
17
  export declare function adapterPackageRoot(): string;
16
18
  /** The pinned handler for this install, or undefined when this build carries none. */
17
19
  export declare function prebuiltHandler(root?: string): PrebuiltHandler | undefined;
20
+ /** The pinned arm64 video function image (an OCI layout tarball), or undefined when none is pinned. */
21
+ export declare function prebuiltVideoImage(root?: string): ReleaseAssetManifest | undefined;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Push a prebuilt single-platform OCI image layout into ECR through the ECR API alone,
3
+ * so deploying the container-image function needs neither Docker nor registry HTTP calls.
4
+ */
5
+ import { ECRClient, type Tag } from "@aws-sdk/client-ecr";
6
+ /** Options for {@link ensureEcrRepository}. */
7
+ export interface EnsureEcrRepositoryOptions {
8
+ ecr?: ECRClient;
9
+ region?: string;
10
+ repositoryName: string;
11
+ /** Set on create. An existing repository is adopted only if it already carries every one. */
12
+ tags?: Tag[];
13
+ }
14
+ /** Create the repository, or adopt an existing one that carries `tags`. */
15
+ export declare function ensureEcrRepository(opts: EnsureEcrRepositoryOptions): Promise<{
16
+ repositoryUri: string;
17
+ }>;
18
+ /** Options for {@link pushImageToEcr}. */
19
+ export interface PushImageToEcrOptions {
20
+ ecr?: ECRClient;
21
+ region?: string;
22
+ /** OCI image layout directory: `index.json` plus `blobs/sha256/<hex>`. */
23
+ layoutDir: string;
24
+ repositoryName: string;
25
+ /** From {@link ensureEcrRepository}. */
26
+ repositoryUri: string;
27
+ tag: string;
28
+ log?: (message: string) => void;
29
+ }
30
+ /** Result of {@link pushImageToEcr}. */
31
+ export interface PushedImage {
32
+ /** `<repositoryUri>@sha256:<manifest digest>`, pinned to exactly the pushed bytes. */
33
+ imageUri: string;
34
+ digest: string;
35
+ /** From the image config, so callers can assert the platform they expect. */
36
+ architecture: string;
37
+ os: string;
38
+ }
39
+ /** Upload the layout's missing blobs, then put its manifest under `tag`. Safe to re-run. */
40
+ export declare function pushImageToEcr(opts: PushImageToEcrOptions): Promise<PushedImage>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/aws-lambda",
3
- "version": "0.8.185",
3
+ "version": "0.8.188",
4
4
  "description": "AWS Lambda adapter for HyperFrames distributed rendering — handler, client-side SDK, and CDK construct.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,6 +12,8 @@
12
12
  "prebuilt/",
13
13
  "scripts/",
14
14
  "!scripts/*.test.ts",
15
+ "!scripts/pin-handler-zip.ts",
16
+ "!scripts/stage-arm64-image.ts",
15
17
  "README.md"
16
18
  ],
17
19
  "type": "module",
@@ -40,6 +42,7 @@
40
42
  "registry": "https://registry.npmjs.org/"
41
43
  },
42
44
  "dependencies": {
45
+ "@aws-sdk/client-ecr": "^3.1146.0",
43
46
  "@aws-sdk/client-lambda": "^3.1146.0",
44
47
  "@aws-sdk/client-s3": "^3.1146.0",
45
48
  "@aws-sdk/client-sfn": "^3.1146.0",
@@ -49,7 +52,7 @@
49
52
  "ffprobe-static": "^3.1.0",
50
53
  "puppeteer-core": "^25.8.0",
51
54
  "tar": "^7.4.3",
52
- "@hyperframes/producer": "^0.8.185"
55
+ "@hyperframes/producer": "^0.8.188"
53
56
  },
54
57
  "devDependencies": {
55
58
  "@types/aws-lambda": "^8.10.146",
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.8.185",
3
- "url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.185/hyperframes-lambda-handler.zip",
4
- "sha256": "b9e2f239accb1c0a50cd402981ce93e4c7533bcc93c5d3483e7d8d5ae1de2884",
5
- "bytes": 129243460
2
+ "version": "0.8.188",
3
+ "url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.188/hyperframes-lambda-handler.zip",
4
+ "sha256": "122a474d12a53df9e0530db4bd8309bcd80c0d2253fb6d2ff8597cafea5e2ba0",
5
+ "bytes": 129249245
6
6
  }
@@ -3,7 +3,7 @@ Transform: AWS::Serverless-2016-10-31
3
3
  Description: >-
4
4
  HyperFrames distributed rendering — Step Functions standard workflow with
5
5
  one Lambda function handling Plan, RenderChunk (fan-out via Map state),
6
- and Assemble. One S3 bucket, alarms for runaway concurrency, Lambda
6
+ and Assemble, or Plan and RenderChunk on an optional arm64 image. One S3 bucket, alarms for runaway concurrency, Lambda
7
7
  errors, and Step Functions execution failures.
8
8
 
9
9
  Built from the handler ZIP at packages/aws-lambda/dist/handler.zip.
@@ -64,29 +64,29 @@ Parameters:
64
64
  chunks than expected; an alarm at 10× the typical workload
65
65
  protects against billing surprises.
66
66
 
67
+ VideoImageUri:
68
+ Type: String
69
+ Default: ""
70
+ Description: >-
71
+ Optional arm64 image (`<ECR repository URI>@sha256:<digest>`) that runs the
72
+ plan and renders the chunks. Audio and assemble stay on the x86 render
73
+ function. Empty: one x86 function does it all.
74
+
67
75
  Conditions:
68
76
  HasReservedConcurrency: !Not [!Equals [!Ref ReservedConcurrency, -1]]
77
+ HasVideoFunction: !Not [!Equals [!Ref VideoImageUri, ""]]
69
78
 
70
79
  Globals:
71
80
  Function:
72
- Runtime: nodejs22.x
73
81
  MemorySize: !Ref LambdaMemoryMb
74
82
  Timeout: !Ref LambdaTimeoutSec
75
- # x86_64 is required for @sparticuz/chromium — its prebuilt
76
- # Chromium ships x86_64-only. Adopters who switch to a custom
77
- # ARM-built chrome-headless-shell can change this to `arm64`, but
78
- # the default ZIP build will fail to launch on Graviton.
79
- Architectures: [x86_64]
80
83
  # Lambda function-level X-Ray tracing. The state machine already
81
84
  # has Tracing.Enabled: true; without this, X-Ray traces would
82
85
  # terminate at the Step Functions → Lambda boundary instead of
83
86
  # following into per-function spans.
84
87
  Tracing: Active
85
- # Cost-allocation tags. Setting these at the Globals level applies
86
- # to every AWS::Serverless::Function in the template — there's
87
- # only one today, but the contract is portable to multi-function
88
- # variants. Bucket + state-machine carry the same tags resource-
89
- # locally because Globals only covers functions.
88
+ # Cost-allocation tags for the render function. Bucket, state machine and
89
+ # video-function.yaml carry the same tags themselves; Globals covers only SAM functions.
90
90
  Tags:
91
91
  Project: !Ref ProjectName
92
92
  HyperFramesComponent: lambda-renderer
@@ -149,14 +149,18 @@ Resources:
149
149
  Description: >-
150
150
  HyperFrames distributed render handler. Dispatches on event.Action.
151
151
  Handler: handler.handler
152
+ Runtime: nodejs22.x
153
+ # The ZIP's @sparticuz/chromium ships x86_64 Chrome only; video-function.yaml runs chunks on arm64.
154
+ Architectures: [x86_64]
152
155
  # Local path is resolved by `sam build` + `sam deploy --resolve-s3`
153
156
  # (or `--s3-bucket`); the resulting CodeUri rewrites to s3://.
154
157
  CodeUri: handler.zip
155
158
  PackageType: Zip
159
+ # With a video function the chunks run there, so the reservation moves with them.
156
160
  ReservedConcurrentExecutions: !If
157
- - HasReservedConcurrency
158
- - !Ref ReservedConcurrency
161
+ - HasVideoFunction
159
162
  - !Ref AWS::NoValue
163
+ - !If [HasReservedConcurrency, !Ref ReservedConcurrency, !Ref AWS::NoValue]
160
164
  EphemeralStorage:
161
165
  Size: 10240
162
166
  Environment:
@@ -184,6 +188,27 @@ Resources:
184
188
  # CreateExportTask). Reference templates shouldn't leak overbroad
185
189
  # IAM into adopters' accounts.
186
190
 
191
+ # ── Optional arm64 chunk renderer: the same handler as a container image ──
192
+ # Nested: `sam deploy` demands an image repository for any image function in this template.
193
+ VideoFunctionStack:
194
+ Type: AWS::CloudFormation::Stack
195
+ Condition: HasVideoFunction
196
+ Properties:
197
+ TemplateURL: video-function.yaml
198
+ Parameters:
199
+ ProjectName: !Ref ProjectName
200
+ ImageUri: !Ref VideoImageUri
201
+ RenderBucket: !Ref RenderBucket
202
+ MemorySize: !Ref LambdaMemoryMb
203
+ Timeout: !Ref LambdaTimeoutSec
204
+ ReservedConcurrency: !Ref ReservedConcurrency
205
+ ChromeSource: !Ref ChromeSource
206
+ Tags:
207
+ - Key: Project
208
+ Value: !Ref ProjectName
209
+ - Key: HyperFramesComponent
210
+ Value: lambda-renderer
211
+
187
212
  # ── CloudWatch log group for the state machine ──────────────────────────
188
213
  # SAM doesn't auto-create one when `LoggingConfiguration` is set, so we
189
214
  # define it explicitly — that way the IAM grant on the state-machine
@@ -249,7 +274,13 @@ Resources:
249
274
  Type: Task
250
275
  Resource: arn:aws:states:::lambda:invoke
251
276
  Parameters:
252
- FunctionName: !GetAtt RenderFunction.Arn
277
+ # Chunks must run the ffmpeg build the plan records, so the plan runs where they do.
278
+ FunctionName:
279
+ !If [
280
+ HasVideoFunction,
281
+ !GetAtt VideoFunctionStack.Outputs.FunctionArn,
282
+ !GetAtt RenderFunction.Arn,
283
+ ]
253
284
  Payload:
254
285
  Action: plan
255
286
  PlanProtocol: v1
@@ -308,7 +339,12 @@ Resources:
308
339
  Type: Task
309
340
  Resource: arn:aws:states:::lambda:invoke
310
341
  Parameters:
311
- FunctionName: !GetAtt RenderFunction.Arn
342
+ FunctionName:
343
+ !If [
344
+ HasVideoFunction,
345
+ !GetAtt VideoFunctionStack.Outputs.FunctionArn,
346
+ !GetAtt RenderFunction.Arn,
347
+ ]
312
348
  Payload:
313
349
  Action: plan
314
350
  PlanProtocol: v2
@@ -424,7 +460,12 @@ Resources:
424
460
  Type: Task
425
461
  Resource: arn:aws:states:::lambda:invoke
426
462
  Parameters:
427
- FunctionName: !GetAtt RenderFunction.Arn
463
+ FunctionName:
464
+ !If [
465
+ HasVideoFunction,
466
+ !GetAtt VideoFunctionStack.Outputs.FunctionArn,
467
+ !GetAtt RenderFunction.Arn,
468
+ ]
428
469
  Payload:
429
470
  Action: renderChunk
430
471
  PlanProtocol: v1
@@ -542,7 +583,12 @@ Resources:
542
583
  Type: Task
543
584
  Resource: arn:aws:states:::lambda:invoke
544
585
  Parameters:
545
- FunctionName: !GetAtt RenderFunction.Arn
586
+ FunctionName:
587
+ !If [
588
+ HasVideoFunction,
589
+ !GetAtt VideoFunctionStack.Outputs.FunctionArn,
590
+ !GetAtt RenderFunction.Arn,
591
+ ]
546
592
  Payload:
547
593
  Action: renderChunk
548
594
  PlanProtocol: v2
@@ -725,7 +771,13 @@ Resources:
725
771
  Statement:
726
772
  - Effect: Allow
727
773
  Action: lambda:InvokeFunction
728
- Resource: !GetAtt RenderFunction.Arn
774
+ Resource:
775
+ - !GetAtt RenderFunction.Arn
776
+ - !If [
777
+ HasVideoFunction,
778
+ !GetAtt VideoFunctionStack.Outputs.FunctionArn,
779
+ !Ref AWS::NoValue,
780
+ ]
729
781
  # A Distributed Map runs each chunk as a child execution of this state machine.
730
782
  # ARNs built by name: !Ref RenderStateMachine here would be a dependency cycle.
731
783
  - PolicyName: RunDistributedMapChildren
@@ -780,7 +832,12 @@ Resources:
780
832
  MetricName: Invocations
781
833
  Dimensions:
782
834
  - Name: FunctionName
783
- Value: !Ref RenderFunction
835
+ Value:
836
+ !If [
837
+ HasVideoFunction,
838
+ !GetAtt VideoFunctionStack.Outputs.FunctionName,
839
+ !Ref RenderFunction,
840
+ ]
784
841
  Statistic: Sum
785
842
  Period: 3600
786
843
  EvaluationPeriods: 1
@@ -813,6 +870,25 @@ Resources:
813
870
  ComparisonOperator: GreaterThanOrEqualToThreshold
814
871
  TreatMissingData: notBreaching
815
872
 
873
+ VideoFunctionErrorsAlarm:
874
+ Type: AWS::CloudWatch::Alarm
875
+ Condition: HasVideoFunction
876
+ Properties:
877
+ AlarmName: !Sub "${ProjectName}-video-function-errors"
878
+ AlarmDescription: >-
879
+ Fires if the arm64 chunk Lambda reports any errors in a 5-minute window.
880
+ Namespace: AWS/Lambda
881
+ MetricName: Errors
882
+ Dimensions:
883
+ - Name: FunctionName
884
+ Value: !GetAtt VideoFunctionStack.Outputs.FunctionName
885
+ Statistic: Sum
886
+ Period: 300
887
+ EvaluationPeriods: 1
888
+ Threshold: 1
889
+ ComparisonOperator: GreaterThanOrEqualToThreshold
890
+ TreatMissingData: notBreaching
891
+
816
892
  # ── CloudWatch alarm: Step Functions execution failures ─────────────────
817
893
  # Fires when a state-machine execution reaches a terminal failure
818
894
  # state (typed non-retryable, retry-exhausted, or top-level timeout).
@@ -853,6 +929,11 @@ Outputs:
853
929
  Export:
854
930
  Name: !Sub "${AWS::StackName}-RenderFunctionArn"
855
931
 
932
+ VideoFunctionArn:
933
+ Condition: HasVideoFunction
934
+ Description: ARN of the arm64 chunk-rendering function.
935
+ Value: !GetAtt VideoFunctionStack.Outputs.FunctionArn
936
+
856
937
  RenderStateMachineArn:
857
938
  Description: ARN of the Step Functions state machine. Pass to `aws stepfunctions start-execution`.
858
939
  Value: !Ref RenderStateMachine
@@ -0,0 +1,108 @@
1
+ AWSTemplateFormatVersion: "2010-09-09"
2
+ Description: >-
3
+ HyperFrames arm64 video renderer: the render handler as a container image, invoked for plan and
4
+ renderChunk. Nested in template.yaml when VideoImageUri is set; plain CloudFormation, because `sam deploy`
5
+ demands an image repository for any top-level image function, even one whose Condition is false.
6
+
7
+ Parameters:
8
+ ProjectName:
9
+ Type: String
10
+ ImageUri:
11
+ Type: String
12
+ RenderBucket:
13
+ Type: String
14
+ MemorySize:
15
+ Type: Number
16
+ Timeout:
17
+ Type: Number
18
+ ReservedConcurrency:
19
+ Type: Number
20
+ ChromeSource:
21
+ Type: String
22
+
23
+ Conditions:
24
+ HasReservedConcurrency: !Not [!Equals [!Ref ReservedConcurrency, -1]]
25
+
26
+ Resources:
27
+ Role:
28
+ Type: AWS::IAM::Role
29
+ Properties:
30
+ AssumeRolePolicyDocument:
31
+ Version: "2012-10-17"
32
+ Statement:
33
+ - Effect: Allow
34
+ Principal:
35
+ Service: lambda.amazonaws.com
36
+ Action: sts:AssumeRole
37
+ ManagedPolicyArns:
38
+ - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
39
+ - !Sub "arn:${AWS::Partition}:iam::aws:policy/AWSXRayDaemonWriteAccess"
40
+ # The render function's S3CrudPolicy plus AbortMultipartUpload, spelled out.
41
+ Policies:
42
+ - PolicyName: render-bucket
43
+ PolicyDocument:
44
+ Version: "2012-10-17"
45
+ Statement:
46
+ - Effect: Allow
47
+ Action:
48
+ - s3:GetObject
49
+ - s3:ListBucket
50
+ - s3:GetBucketLocation
51
+ - s3:GetObjectVersion
52
+ - s3:PutObject
53
+ - s3:PutObjectAcl
54
+ - s3:GetLifecycleConfiguration
55
+ - s3:PutLifecycleConfiguration
56
+ - s3:DeleteObject
57
+ - s3:AbortMultipartUpload
58
+ Resource:
59
+ - !Sub "arn:${AWS::Partition}:s3:::${RenderBucket}"
60
+ - !Sub "arn:${AWS::Partition}:s3:::${RenderBucket}/*"
61
+ # The plan sizes an unreserved render to the account's concurrency.
62
+ - Effect: Allow
63
+ Action: lambda:GetAccountSettings
64
+ Resource: "*"
65
+ Tags:
66
+ - Key: Project
67
+ Value: !Ref ProjectName
68
+ - Key: HyperFramesComponent
69
+ Value: lambda-renderer
70
+
71
+ Function:
72
+ Type: AWS::Lambda::Function
73
+ Properties:
74
+ FunctionName: !Sub "${ProjectName}-video"
75
+ Description: HyperFrames distributed render handler on arm64, invoked for plan and renderChunk.
76
+ PackageType: Image
77
+ Code:
78
+ ImageUri: !Ref ImageUri
79
+ Architectures: [arm64]
80
+ Role: !GetAtt Role.Arn
81
+ MemorySize: !Ref MemorySize
82
+ Timeout: !Ref Timeout
83
+ ReservedConcurrentExecutions: !If
84
+ - HasReservedConcurrency
85
+ - !Ref ReservedConcurrency
86
+ - !Ref AWS::NoValue
87
+ EphemeralStorage:
88
+ Size: 10240
89
+ TracingConfig:
90
+ Mode: Active
91
+ Environment:
92
+ Variables:
93
+ NODE_OPTIONS: "--enable-source-maps"
94
+ HYPERFRAMES_LAMBDA_CHROME_SOURCE: !Ref ChromeSource
95
+ TMPDIR: /tmp
96
+ HYPERFRAMES_RENDER_BUCKET: !Ref RenderBucket
97
+ HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY: !Ref ReservedConcurrency
98
+ Tags:
99
+ - Key: Project
100
+ Value: !Ref ProjectName
101
+ - Key: HyperFramesComponent
102
+ Value: lambda-renderer
103
+
104
+ Outputs:
105
+ FunctionArn:
106
+ Value: !GetAtt Function.Arn
107
+ FunctionName:
108
+ Value: !Ref Function
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "0.8.188",
3
+ "url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.188/hyperframes-lambda-video-arm64.oci.tar",
4
+ "sha256": "71f36a3f1b8db5fd6704cadaf1ed46dc0ce84eb890c6f119eb1b3b8e8cf69244",
5
+ "bytes": 244628480
6
+ }
@@ -1,11 +1,15 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  const CHECKOUT_CODE_URI = "CodeUri: ../../packages/aws-lambda/dist/handler.zip";
5
5
 
6
- /** Writes `<outDir>/template.yaml`: the example SAM template, pointed at a `handler.zip` beside it. */
6
+ /**
7
+ * Writes `<outDir>/template.yaml`, the example SAM template pointed at a `handler.zip` beside it, and
8
+ * the `video-function.yaml` it nests.
9
+ */
7
10
  export function writePrebuiltTemplate(repoRoot, outDir) {
8
- const template = readFileSync(join(repoRoot, "examples", "aws-lambda", "template.yaml"), "utf-8");
11
+ const examples = join(repoRoot, "examples", "aws-lambda");
12
+ const template = readFileSync(join(examples, "template.yaml"), "utf-8");
9
13
  if (template.split(CHECKOUT_CODE_URI).length !== 2) {
10
14
  throw new Error(
11
15
  `[prebuilt-template] expected exactly one "${CHECKOUT_CODE_URI}" in the template`,
@@ -16,4 +20,5 @@ export function writePrebuiltTemplate(repoRoot, outDir) {
16
20
  join(outDir, "template.yaml"),
17
21
  template.replace(CHECKOUT_CODE_URI, "CodeUri: handler.zip"),
18
22
  );
23
+ copyFileSync(join(examples, "video-function.yaml"), join(outDir, "video-function.yaml"));
19
24
  }
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env bun
2
- // Release step: writes the handler ZIP's pin (asset URL + sha256) and the SAM template into the
3
- // package. A retried release run reuses the ZIP already on the release.
4
- import { createHash } from "node:crypto";
5
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
- import { dirname, join, resolve } from "node:path";
7
- import { fileURLToPath } from "node:url";
8
- import {
9
- HANDLER_MANIFEST_FILE,
10
- HANDLER_ZIP_ASSET,
11
- type HandlerZipManifest,
12
- PREBUILT_DIR,
13
- } from "../src/sdk/prebuiltHandler.js";
14
- import { writePrebuiltTemplate } from "./prebuilt-template.mjs";
15
-
16
- export function pinHandlerZip(repoRoot: string): HandlerZipManifest {
17
- const pkgDir = join(repoRoot, "packages", "aws-lambda");
18
- const pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf-8")) as {
19
- version: string;
20
- repository: { url: string };
21
- };
22
- const asset = join(repoRoot, "dist", HANDLER_ZIP_ASSET);
23
- const built = join(pkgDir, "dist", "handler.zip");
24
- if (existsSync(built)) {
25
- mkdirSync(dirname(asset), { recursive: true });
26
- renameSync(built, asset);
27
- rmSync(`${built}.manifest.json`, { force: true });
28
- }
29
- const bytes = readFileSync(asset);
30
- const sha256 = createHash("sha256").update(bytes).digest("hex");
31
- writeFileSync(asset.replace(/\.zip$/, ".sha256"), `${sha256} ${HANDLER_ZIP_ASSET}\n`);
32
-
33
- const outDir = join(pkgDir, PREBUILT_DIR);
34
- writePrebuiltTemplate(repoRoot, outDir);
35
- const manifest: HandlerZipManifest = {
36
- version: pkg.version,
37
- url: `${pkg.repository.url}/releases/download/v${pkg.version}/${HANDLER_ZIP_ASSET}`,
38
- sha256,
39
- bytes: bytes.length,
40
- };
41
- writeFileSync(join(outDir, HANDLER_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
42
- return manifest;
43
- }
44
-
45
- if (import.meta.main) {
46
- const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
47
- console.log(JSON.stringify(pinHandlerZip(repoRoot), null, 2));
48
- }