@hyperframes/aws-lambda 0.8.210 → 0.8.212

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.
@@ -24,7 +24,7 @@ import {
24
24
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
25
25
  import { availableParallelism, tmpdir as tmpdir2 } from "node:os";
26
26
  import { basename, join as join2 } from "node:path";
27
- import { GetAccountSettingsCommand, LambdaClient } from "@aws-sdk/client-lambda";
27
+ import { GetAccountSettingsCommand, InvokeCommand, LambdaClient } from "@aws-sdk/client-lambda";
28
28
  import { S3Client } from "@aws-sdk/client-s3";
29
29
  import {
30
30
  assemble,
@@ -119,6 +119,7 @@ import {
119
119
  } from "@aws-sdk/client-s3";
120
120
  import { buildStitchedHeader, readMp4Layout } from "@hyperframes/producer/distributed";
121
121
  var MIN_PART_BYTES = 5 * 1024 * 1024;
122
+ var HELPER_BATCH_BYTES = 32 * 1024 * 1024;
122
123
  var segmentSize = (s) => "bytes" in s ? s.bytes.length : s.end - s.start;
123
124
  function planParts(segments, min = MIN_PART_BYTES) {
124
125
  const parts = [];
@@ -174,32 +175,79 @@ async function sendPart(s3, part, partNumber, target) {
174
175
  );
175
176
  return result.ETag;
176
177
  }
177
- async function sendParts(s3, parts, target) {
178
- const settled = await Promise.allSettled(
179
- parts.map(async (part, index) => {
180
- const etag = await sendPart(s3, part, index + 1, target);
181
- if (!etag) throw new Error(`[s3Stitch] part ${index + 1} returned no ETag`);
182
- return etag;
183
- })
184
- );
178
+ async function settleAll(tasks) {
179
+ const settled = await Promise.allSettled(tasks);
185
180
  const failure = settled.find(
186
181
  (result) => result.status === "rejected"
187
182
  );
188
183
  if (failure) throw failure.reason;
189
- return settled.map((result, index) => ({
190
- ETag: result.value,
191
- PartNumber: index + 1
192
- }));
184
+ return settled.map((result) => result.value);
185
+ }
186
+ var rangesOnly = (part) => "upload" in part && part.upload.every((s) => "uri" in s) ? part.upload : void 0;
187
+ var rangeBytes = (ranges) => ranges.reduce((total, r) => total + segmentSize(r), 0);
188
+ function helperBatches(parts, maxBatches = Infinity) {
189
+ const total = parts.reduce((sum, part) => sum + rangeBytes(rangesOnly(part) ?? []), 0);
190
+ const batchBytes = Math.max(HELPER_BATCH_BYTES, Math.ceil(total / maxBatches));
191
+ const batches = [];
192
+ let batch = [];
193
+ let bytes = 0;
194
+ parts.forEach((part, index) => {
195
+ const ranges = rangesOnly(part);
196
+ if (!ranges) return;
197
+ batch.push({ partNumber: index + 1, ranges });
198
+ bytes += rangeBytes(ranges);
199
+ if (bytes < batchBytes) return;
200
+ batches.push(batch);
201
+ batch = [];
202
+ bytes = 0;
203
+ });
204
+ if (batch.length) batches.push(batch);
205
+ return batches;
206
+ }
207
+ async function sendRangeParts(s3, target, parts) {
208
+ return settleAll(
209
+ parts.map(async ({ partNumber, ranges }) => {
210
+ const etag = await sendPart(s3, { upload: ranges }, partNumber, target);
211
+ if (!etag) throw new Error(`[s3Stitch] part ${partNumber} returned no ETag`);
212
+ return etag;
213
+ })
214
+ );
193
215
  }
194
- async function uploadParts(s3, parts, destUri, contentType) {
216
+ async function sendParts(s3, parts, target, helper) {
217
+ const batches = helper ? helperBatches(parts, helper.maxInvocations) : [];
218
+ const remote = new Set(batches.flat().map((p) => p.partNumber));
219
+ const local = parts.flatMap(
220
+ (part, index) => remote.has(index + 1) ? [] : [{ partNumber: index + 1, part }]
221
+ );
222
+ const done = await settleAll([
223
+ ...local.map(async ({ partNumber, part }) => [
224
+ { PartNumber: partNumber, ETag: await sendPart(s3, part, partNumber, target) }
225
+ ]),
226
+ ...batches.map(async (batch) => {
227
+ const etags = await helper.send(target, batch);
228
+ return batch.map((p, i) => ({ PartNumber: p.partNumber, ETag: etags[i] }));
229
+ })
230
+ ]);
231
+ const completed = done.flat().sort((a, b) => a.PartNumber - b.PartNumber);
232
+ const missing = completed.find((p) => !p.ETag);
233
+ if (missing || completed.length !== parts.length) {
234
+ throw new Error(`[s3Stitch] part ${missing?.PartNumber ?? "?"} returned no ETag`);
235
+ }
236
+ return completed;
237
+ }
238
+ async function uploadParts(s3, parts, destUri, contentType, helper) {
195
239
  const { bucket, key } = parseS3Uri(destUri);
196
240
  await multipartUpload(
197
241
  s3,
198
242
  { Bucket: bucket, Key: key, ContentType: contentType },
199
- (target) => sendParts(s3, parts, target)
243
+ (target) => sendParts(s3, parts, target, helper)
200
244
  );
201
245
  }
202
- async function stitchMp4InS3(s3, chunkUris, destUri, { audioUri, minPartBytes = MIN_PART_BYTES } = {}) {
246
+ async function stitchMp4InS3(s3, chunkUris, destUri, {
247
+ audioUri,
248
+ minPartBytes = MIN_PART_BYTES,
249
+ helper
250
+ } = {}) {
203
251
  const sourceUris = audioUri ? [audioUri, ...chunkUris] : chunkUris;
204
252
  const layouts = await Promise.all(
205
253
  sourceUris.map((uri) => readMp4Layout((start, end) => getRange(s3, uri, start, end)))
@@ -214,12 +262,24 @@ async function stitchMp4InS3(s3, chunkUris, destUri, { audioUri, minPartBytes =
214
262
  }))
215
263
  ];
216
264
  const parts = planParts(segments, minPartBytes);
217
- await uploadParts(s3, parts, destUri, "video/mp4");
265
+ let helperFallbacks = 0;
266
+ const helperOrLocal = helper && {
267
+ maxInvocations: helper.maxInvocations,
268
+ // A throttled helper (another render may hold the reservation) costs time, not the render.
269
+ send: (target, batch) => helper.send(target, batch).catch((error) => {
270
+ helperFallbacks++;
271
+ console.warn(`[s3Stitch] helper failed, building ${batch.length} parts here:`, error);
272
+ return sendRangeParts(s3, target, batch);
273
+ })
274
+ };
275
+ await uploadParts(s3, parts, destUri, "video/mp4", helperOrLocal);
218
276
  const uploaded = parts.flatMap((part) => "upload" in part ? part.upload : []);
219
277
  return {
220
278
  fileSize: segments.reduce((total, s) => total + segmentSize(s), 0),
221
279
  parts: parts.length,
222
- downloadedBytes: uploaded.reduce((total, s) => total + ("uri" in s ? segmentSize(s) : 0), 0)
280
+ downloadedBytes: uploaded.reduce((total, s) => total + ("uri" in s ? segmentSize(s) : 0), 0),
281
+ helpers: helper ? helperBatches(parts, helper.maxInvocations).length : 0,
282
+ helperFallbacks
223
283
  };
224
284
  }
225
285
 
@@ -350,6 +410,9 @@ async function handler(event, deps) {
350
410
  await ensureChromePath(deps);
351
411
  return { Action: "warm" };
352
412
  }
413
+ if (event.Action === "stitchParts") {
414
+ return await handleStitchParts(event, deps);
415
+ }
353
416
  const unwrapped = unwrapEvent(event);
354
417
  validatePlanProtocolShape(unwrapped);
355
418
  validateEventS3Uris(unwrapped);
@@ -508,9 +571,13 @@ async function resolveWorkerConcurrency(event, deps) {
508
571
  return DEFAULT_MAX_PARALLEL_CHUNKS;
509
572
  }
510
573
  }
511
- async function functionConcurrency(lambda = new LambdaClient({})) {
574
+ function reservedConcurrency() {
512
575
  const reserved = Number(process.env.HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY);
513
- if (reserved > 0) return reserved;
576
+ return reserved > 0 ? reserved : void 0;
577
+ }
578
+ async function functionConcurrency(lambda = new LambdaClient({})) {
579
+ const reserved = reservedConcurrency();
580
+ if (reserved) return reserved;
514
581
  const { AccountLimit } = await lambda.send(new GetAccountSettingsCommand({}));
515
582
  const unreserved = AccountLimit?.UnreservedConcurrentExecutions;
516
583
  if (unreserved === void 0) throw new Error("no UnreservedConcurrentExecutions in the reply");
@@ -869,7 +936,7 @@ async function handleAssembleV2(event, deps) {
869
936
  throw error;
870
937
  }
871
938
  if (chunksAreStitchedMp4(planDir, event.Cfr === true)) {
872
- return await stitchChunksInS3(s3, event, manifest, started);
939
+ return await stitchChunksInS3(s3, event, manifest, started, deps?.lambda);
873
940
  }
874
941
  let audioPath = resolvePlanAudioPath(planDir);
875
942
  if (event.AudioS3Uri !== null) {
@@ -909,7 +976,7 @@ async function handleAssembleV2(event, deps) {
909
976
  cleanupDir(work);
910
977
  }
911
978
  }
912
- async function stitchChunksInS3(s3, event, plan2, started) {
979
+ async function stitchChunksInS3(s3, event, plan2, started, lambda) {
913
980
  if (event.ChunkS3Uris.length !== plan2.chunkCount) {
914
981
  throw new Error(
915
982
  `[handler] ${event.ChunkS3Uris.length} chunk URIs for a plan of ${plan2.chunkCount} chunks`
@@ -917,7 +984,8 @@ async function stitchChunksInS3(s3, event, plan2, started) {
917
984
  }
918
985
  const stitchStarted = Date.now();
919
986
  const stitched = await stitchMp4InS3(s3, event.ChunkS3Uris, event.OutputS3Uri, {
920
- audioUri: event.AudioS3Uri ?? void 0
987
+ audioUri: event.AudioS3Uri ?? void 0,
988
+ helper: stitchHelper(lambda)
921
989
  });
922
990
  logEvent({
923
991
  event: "assemble_stages",
@@ -925,7 +993,9 @@ async function stitchChunksInS3(s3, event, plan2, started) {
925
993
  planMs: stitchStarted - started,
926
994
  stitchMs: Date.now() - stitchStarted,
927
995
  parts: stitched.parts,
928
- downloadedBytes: stitched.downloadedBytes
996
+ downloadedBytes: stitched.downloadedBytes,
997
+ helpers: stitched.helpers,
998
+ helperFallbacks: stitched.helperFallbacks
929
999
  });
930
1000
  return {
931
1001
  Action: "assemble",
@@ -1035,10 +1105,47 @@ function getEventS3Uris(event) {
1035
1105
  ].filter((u) => u != null);
1036
1106
  }
1037
1107
  }
1108
+ function stitchHelper(lambda) {
1109
+ const functionName = process.env.HYPERFRAMES_STITCH_FUNCTION?.trim();
1110
+ if (!functionName) return void 0;
1111
+ const client = lambda ?? new LambdaClient({});
1112
+ const reserved = reservedConcurrency();
1113
+ const maxInvocations = reserved ? Math.max(1, reserved - 1) : Infinity;
1114
+ const send = async (target, parts) => {
1115
+ const event = { Action: "stitchParts", ...target, Parts: parts };
1116
+ const response = await client.send(
1117
+ new InvokeCommand({ FunctionName: functionName, Payload: JSON.stringify(event) })
1118
+ );
1119
+ const body = JSON.parse(
1120
+ new TextDecoder().decode(response.Payload)
1121
+ );
1122
+ if (response.FunctionError) {
1123
+ const error = new Error(
1124
+ `[handler] stitchParts on ${functionName} failed: ${body.errorMessage ?? response.FunctionError}`
1125
+ );
1126
+ if (body.errorType) error.name = body.errorType;
1127
+ throw error;
1128
+ }
1129
+ return body.ETags;
1130
+ };
1131
+ return { send, maxInvocations };
1132
+ }
1133
+ async function handleStitchParts(event, deps) {
1134
+ const { Bucket, Key, UploadId, Parts } = event;
1135
+ assertAllowedBuckets([
1136
+ `s3://${Bucket}/${Key}`,
1137
+ ...Parts.flatMap((part) => part.ranges.map((r) => r.uri))
1138
+ ]);
1139
+ const ETags = await sendRangeParts(deps?.s3 ?? getS3Client(), { Bucket, Key, UploadId }, Parts);
1140
+ return { Action: "stitchParts", ETags };
1141
+ }
1038
1142
  function validateEventS3Uris(event) {
1143
+ assertAllowedBuckets(getEventS3Uris(event));
1144
+ }
1145
+ function assertAllowedBuckets(uris) {
1039
1146
  const allowedBucket = process.env.HYPERFRAMES_RENDER_BUCKET?.trim();
1040
1147
  if (!allowedBucket) return;
1041
- for (const uri of getEventS3Uris(event)) {
1148
+ for (const uri of uris) {
1042
1149
  const { bucket } = parseS3Uri(uri);
1043
1150
  if (bucket !== allowedBucket) {
1044
1151
  const err = new Error(
@@ -1089,5 +1196,6 @@ export {
1089
1196
  resolveChromeArgs,
1090
1197
  S3PlanV2ArtifactPublisher,
1091
1198
  handler,
1092
- unwrapEvent
1199
+ unwrapEvent,
1200
+ stitchHelper
1093
1201
  };
package/dist/cdk/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  // src/cdk/HyperframesRenderStack.ts
7
7
  import { dirname, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { Duration, RemovalPolicy, Size } from "aws-cdk-lib";
9
+ import { ArnFormat, Duration, RemovalPolicy, Size, Stack } from "aws-cdk-lib";
10
10
  import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
11
11
  import * as iam from "aws-cdk-lib/aws-iam";
12
12
  import * as lambda from "aws-cdk-lib/aws-lambda";
@@ -72,13 +72,26 @@ var HyperframesRenderStack = class extends Construct {
72
72
  TMPDIR: "/tmp",
73
73
  HYPERFRAMES_LAMBDA_CHROME_SOURCE: chromeSource,
74
74
  HYPERFRAMES_RENDER_BUCKET: this.bucket.bucketName,
75
- HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY: String(props.reservedConcurrency ?? -1)
75
+ HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY: String(props.reservedConcurrency ?? -1),
76
+ HYPERFRAMES_STITCH_FUNCTION: `${projectName}-render`
76
77
  }
77
78
  });
78
79
  this.bucket.grantReadWrite(this.renderFunction);
79
80
  this.renderFunction.addToRolePolicy(
80
81
  new iam.PolicyStatement({ actions: ["lambda:GetAccountSettings"], resources: ["*"] })
81
82
  );
83
+ const renderFunctionArnWithoutRoleCycle = Stack.of(this).formatArn({
84
+ service: "lambda",
85
+ resource: "function",
86
+ resourceName: `${projectName}-render`,
87
+ arnFormat: ArnFormat.COLON_RESOURCE_NAME
88
+ });
89
+ this.renderFunction.addToRolePolicy(
90
+ new iam.PolicyStatement({
91
+ actions: ["lambda:InvokeFunction"],
92
+ resources: [renderFunctionArnWithoutRoleCycle]
93
+ })
94
+ );
82
95
  const stateMachineLogGroup = new logs.LogGroup(this, "RenderStateMachineLogGroup", {
83
96
  logGroupName: `/aws/states/${projectName}-render`,
84
97
  retention: logs.RetentionDays.ONE_MONTH,
package/dist/events.d.ts CHANGED
@@ -22,6 +22,21 @@ export type LambdaAction = "plan" | "audio" | "renderChunk" | "assemble";
22
22
  export interface WarmEvent {
23
23
  Action: "warm";
24
24
  }
25
+ /** Assemble hands a helper part of the final mp4's multipart upload: each part's S3 ranges, in order. */
26
+ export interface StitchPartsEvent {
27
+ Action: "stitchParts";
28
+ Bucket: string;
29
+ Key: string;
30
+ UploadId: string;
31
+ Parts: {
32
+ partNumber: number;
33
+ ranges: {
34
+ uri: string;
35
+ start: number;
36
+ end: number;
37
+ }[];
38
+ }[];
39
+ }
25
40
  /** Transport protocol selected for one complete distributed render. */
26
41
  export type LambdaPlanProtocol = "v1" | "v2";
27
42
  /**
@@ -32,7 +47,7 @@ export type LambdaPlanProtocol = "v1" | "v2";
32
47
  * handler unwraps both `event.Payload` and `event.Input` before
33
48
  * dispatching.
34
49
  */
35
- export type LambdaEvent = PlanEvent | AudioEvent | RenderChunkEvent | AssembleEvent | WarmEvent | {
50
+ export type LambdaEvent = PlanEvent | AudioEvent | RenderChunkEvent | AssembleEvent | WarmEvent | StitchPartsEvent | {
36
51
  Payload: LambdaEvent;
37
52
  } | {
38
53
  Input: LambdaEvent;
@@ -205,4 +220,8 @@ export interface AssembleLambdaResult {
205
220
  export interface WarmLambdaResult {
206
221
  Action: "warm";
207
222
  }
208
- export type LambdaResult = WarmLambdaResult | PlanLambdaResult | AudioLambdaResult | RenderChunkLambdaResult | AssembleLambdaResult;
223
+ export interface StitchPartsLambdaResult {
224
+ Action: "stitchParts";
225
+ ETags: string[];
226
+ }
227
+ export type LambdaResult = WarmLambdaResult | StitchPartsLambdaResult | PlanLambdaResult | AudioLambdaResult | RenderChunkLambdaResult | AssembleLambdaResult;
package/dist/handler.d.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  import { LambdaClient } from "@aws-sdk/client-lambda";
14
14
  import { S3Client } from "@aws-sdk/client-s3";
15
15
  import { assemble, buildPlanAudio, type ChunkRenderer, plan, planV2WithPublisher } from "@hyperframes/producer/distributed";
16
+ import { type StitchHelper } from "./s3Stitch.js";
16
17
  import type { AssembleEvent, AudioEvent, LambdaEvent, LambdaResult, PlanEvent, RenderChunkEvent } from "./events.js";
17
18
  /**
18
19
  * Optional injection points used by the handler's unit tests. Production
@@ -49,3 +50,8 @@ export interface HandlerDeps {
49
50
  */
50
51
  export declare function handler(event: LambdaEvent, deps?: HandlerDeps): Promise<LambdaResult>;
51
52
  export declare function unwrapEvent(event: LambdaEvent): PlanEvent | AudioEvent | RenderChunkEvent | AssembleEvent;
53
+ /**
54
+ * Parts S3 cannot copy go to the function the stack names: it just ran the chunks, so it is warm,
55
+ * and each invocation brings its own network link.
56
+ */
57
+ export declare function stitchHelper(lambda?: LambdaClient): StitchHelper | undefined;
package/dist/handler.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  handler,
3
+ stitchHelper,
3
4
  unwrapEvent
4
- } from "./_chunks/chunk-OTRGQBLD.js";
5
+ } from "./_chunks/chunk-QU5TSGIK.js";
5
6
  import "./_chunks/chunk-6TIVMKTV.js";
6
7
  import "./_chunks/chunk-4CK3RKM2.js";
7
8
  export {
8
9
  handler,
10
+ stitchHelper,
9
11
  unwrapEvent
10
12
  };
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  resolveChromeExecutablePath,
7
7
  resolveChromeSource,
8
8
  unwrapEvent
9
- } from "./_chunks/chunk-OTRGQBLD.js";
9
+ } from "./_chunks/chunk-QU5TSGIK.js";
10
10
  import {
11
11
  InvalidConfigError,
12
12
  computeRenderCost,
@@ -1,4 +1,5 @@
1
1
  import { type S3Client } from "@aws-sdk/client-s3";
2
+ import { type MultipartTarget } from "./s3Transport.js";
2
3
  /** Bytes `[start, end)` of an S3 object. */
3
4
  interface RangeSegment {
4
5
  uri: string;
@@ -13,17 +14,31 @@ export type Part = {
13
14
  } | {
14
15
  upload: Segment[];
15
16
  };
17
+ export interface RangePart {
18
+ partNumber: number;
19
+ ranges: RangeSegment[];
20
+ }
21
+ export type SendRangeParts = (target: MultipartTarget, parts: RangePart[]) => Promise<string[]>;
22
+ export interface StitchHelper {
23
+ send: SendRangeParts;
24
+ maxInvocations: number;
25
+ }
16
26
  export declare function planParts(segments: readonly Segment[], min?: number): Part[];
27
+ export declare function helperBatches(parts: readonly Part[], maxBatches?: number): RangePart[][];
28
+ export declare function sendRangeParts(s3: S3Client, target: MultipartTarget, parts: readonly RangePart[]): Promise<string[]>;
17
29
  /** Writes `parts` in order to `destUri`; a failure aborts the upload so no parts linger. */
18
- export declare function uploadParts(s3: S3Client, parts: readonly Part[], destUri: string, contentType: string): Promise<void>;
30
+ export declare function uploadParts(s3: S3Client, parts: readonly Part[], destUri: string, contentType: string, helper?: StitchHelper): Promise<void>;
19
31
  export interface StitchResult {
20
32
  fileSize: number;
21
33
  parts: number;
22
34
  downloadedBytes: number;
35
+ helpers: number;
36
+ helperFallbacks: number;
23
37
  }
24
38
  /** One faststart mp4 at `destUri` from the stitched mp4 chunks at `chunkUris`, plus `audioUri`'s track when given. */
25
- export declare function stitchMp4InS3(s3: S3Client, chunkUris: readonly string[], destUri: string, { audioUri, minPartBytes }?: {
39
+ export declare function stitchMp4InS3(s3: S3Client, chunkUris: readonly string[], destUri: string, { audioUri, minPartBytes, helper, }?: {
26
40
  audioUri?: string;
27
41
  minPartBytes?: number;
42
+ helper?: StitchHelper;
28
43
  }): Promise<StitchResult>;
29
44
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/aws-lambda",
3
- "version": "0.8.210",
3
+ "version": "0.8.212",
4
4
  "description": "AWS Lambda adapter for HyperFrames distributed rendering — handler, client-side SDK, and CDK construct.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,7 +52,7 @@
52
52
  "ffprobe-static": "^3.1.0",
53
53
  "puppeteer-core": "^25.8.0",
54
54
  "tar": "^7.4.3",
55
- "@hyperframes/producer": "^0.8.210"
55
+ "@hyperframes/producer": "^0.8.212"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/aws-lambda": "^8.10.146",
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.8.210",
3
- "url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.210/hyperframes-lambda-handler.zip",
4
- "sha256": "4c2431e91410c3cd0a1ed7be21e6964a1ca7a495f38926ec65a52275b4b103a1",
5
- "bytes": 129297206
2
+ "version": "0.8.212",
3
+ "url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.212/hyperframes-lambda-handler.zip",
4
+ "sha256": "eee7134320fd223909faf394d860fb147fb74cff6e9f8b929754019a7b996c6e",
5
+ "bytes": 129300443
6
6
  }
@@ -170,9 +170,21 @@ Resources:
170
170
  TMPDIR: /tmp
171
171
  HYPERFRAMES_RENDER_BUCKET: !Ref RenderBucket
172
172
  HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY: !Ref ReservedConcurrency
173
+ # Assemble spreads the parts S3 cannot copy across the warm function that ran the chunks.
174
+ HYPERFRAMES_STITCH_FUNCTION: !If
175
+ - HasVideoFunction
176
+ - !Sub "${ProjectName}-video"
177
+ - !Sub "${ProjectName}-render"
173
178
  Policies:
174
179
  - S3CrudPolicy:
175
180
  BucketName: !Ref RenderBucket
181
+ - Statement:
182
+ - Effect: Allow
183
+ Action: lambda:InvokeFunction
184
+ Resource: !If
185
+ - HasVideoFunction
186
+ - !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${ProjectName}-video"
187
+ - !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${ProjectName}-render"
176
188
  - Statement:
177
189
  - Effect: Allow
178
190
  Action: s3:AbortMultipartUpload
@@ -62,6 +62,10 @@ Resources:
62
62
  - Effect: Allow
63
63
  Action: lambda:GetAccountSettings
64
64
  Resource: "*"
65
+ # Stitch helpers, should this function ever run an assemble.
66
+ - Effect: Allow
67
+ Action: lambda:InvokeFunction
68
+ Resource: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${ProjectName}-video"
65
69
  Tags:
66
70
  - Key: Project
67
71
  Value: !Ref ProjectName
@@ -95,6 +99,7 @@ Resources:
95
99
  TMPDIR: /tmp
96
100
  HYPERFRAMES_RENDER_BUCKET: !Ref RenderBucket
97
101
  HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY: !Ref ReservedConcurrency
102
+ HYPERFRAMES_STITCH_FUNCTION: !Sub "${ProjectName}-video"
98
103
  Tags:
99
104
  - Key: Project
100
105
  Value: !Ref ProjectName
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.8.210",
3
- "url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.210/hyperframes-lambda-video-arm64.oci.tar",
4
- "sha256": "56754b3b0ad55bef79e94c59ebb19e3f63c0de0d8011837aab4ae6214ca1bf6a",
5
- "bytes": 244675584
2
+ "version": "0.8.212",
3
+ "url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.212/hyperframes-lambda-video-arm64.oci.tar",
4
+ "sha256": "9a05da911e95e420c8e62348e4e3f024bfcbe4873f90472cab10c64163643177",
5
+ "bytes": 244680192
6
6
  }