@hyperframes/aws-lambda 0.8.173 → 0.8.174
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/cdk/index.js +39 -7
- package/dist/events.d.ts +1 -0
- package/dist/handler.d.ts +3 -0
- package/dist/handler.js +220 -21
- package/dist/index.js +295 -22
- package/dist/s3Stitch.d.ts +26 -0
- package/dist/sdk/getRenderProgress.d.ts +9 -5
- package/dist/sdk/index.js +75 -1
- package/package.json +3 -2
- package/scripts/build-zip.ts +1 -0
package/dist/cdk/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { Duration, RemovalPolicy, Size } from "aws-cdk-lib";
|
|
5
5
|
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
|
|
6
|
+
import * as iam from "aws-cdk-lib/aws-iam";
|
|
6
7
|
import * as lambda from "aws-cdk-lib/aws-lambda";
|
|
7
8
|
import * as logs from "aws-cdk-lib/aws-logs";
|
|
8
9
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
@@ -59,10 +60,14 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
59
60
|
NODE_OPTIONS: "--enable-source-maps",
|
|
60
61
|
TMPDIR: "/tmp",
|
|
61
62
|
HYPERFRAMES_LAMBDA_CHROME_SOURCE: chromeSource,
|
|
62
|
-
HYPERFRAMES_RENDER_BUCKET: this.bucket.bucketName
|
|
63
|
+
HYPERFRAMES_RENDER_BUCKET: this.bucket.bucketName,
|
|
64
|
+
HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY: String(props.reservedConcurrency ?? -1)
|
|
63
65
|
}
|
|
64
66
|
});
|
|
65
67
|
this.bucket.grantReadWrite(this.renderFunction);
|
|
68
|
+
this.renderFunction.addToRolePolicy(
|
|
69
|
+
new iam.PolicyStatement({ actions: ["lambda:GetAccountSettings"], resources: ["*"] })
|
|
70
|
+
);
|
|
66
71
|
const stateMachineLogGroup = new logs.LogGroup(this, "RenderStateMachineLogGroup", {
|
|
67
72
|
logGroupName: `/aws/states/${projectName}-render`,
|
|
68
73
|
retention: logs.RetentionDays.ONE_MONTH,
|
|
@@ -143,6 +148,7 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
143
148
|
"PlanV2IntegrityError",
|
|
144
149
|
"PLAN_ARTIFACT_DIGEST_MISMATCH",
|
|
145
150
|
"FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED",
|
|
151
|
+
"CHUNK_EXCEEDS_WORKER_TIMEOUT",
|
|
146
152
|
"ChromeBinaryUnavailableError"
|
|
147
153
|
];
|
|
148
154
|
const NON_RETRYABLE_CHUNK = [
|
|
@@ -174,6 +180,14 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
174
180
|
"PLAN_ARTIFACT_DIGEST_MISMATCH",
|
|
175
181
|
"ChromeBinaryUnavailableError"
|
|
176
182
|
];
|
|
183
|
+
const THROTTLED_RETRY = {
|
|
184
|
+
errors: ["Lambda.TooManyRequestsException"],
|
|
185
|
+
interval: Duration.seconds(5),
|
|
186
|
+
maxAttempts: 10,
|
|
187
|
+
backoffRate: 2,
|
|
188
|
+
maxDelay: Duration.seconds(60),
|
|
189
|
+
jitterStrategy: sfn.JitterType.FULL
|
|
190
|
+
};
|
|
177
191
|
const plan = new tasks.LambdaInvoke(this, "Plan", {
|
|
178
192
|
lambdaFunction: this.renderFunction,
|
|
179
193
|
payload: sfn.TaskInput.fromObject({
|
|
@@ -188,6 +202,7 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
188
202
|
"PlanS3Uri.$": "$.Payload.PlanS3Uri",
|
|
189
203
|
"PlanHash.$": "$.Payload.PlanHash",
|
|
190
204
|
"ChunkCount.$": "$.Payload.ChunkCount",
|
|
205
|
+
"MaxChunkConcurrency.$": "$.Payload.MaxChunkConcurrency",
|
|
191
206
|
"Format.$": "$.Payload.Format",
|
|
192
207
|
"HasAudio.$": "$.Payload.HasAudio",
|
|
193
208
|
"AudioS3Uri.$": "$.Payload.AudioS3Uri"
|
|
@@ -198,6 +213,7 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
198
213
|
errors: NON_RETRYABLE_PLAN,
|
|
199
214
|
maxAttempts: 0
|
|
200
215
|
});
|
|
216
|
+
plan.addRetry(THROTTLED_RETRY);
|
|
201
217
|
plan.addRetry({
|
|
202
218
|
errors: ["States.ALL"],
|
|
203
219
|
interval: Duration.seconds(2),
|
|
@@ -220,12 +236,14 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
220
236
|
"PlanV2ArtifactS3Prefix.$": "$.Payload.PlanV2ArtifactS3Prefix",
|
|
221
237
|
"PlanHash.$": "$.Payload.PlanHash",
|
|
222
238
|
"ChunkCount.$": "$.Payload.ChunkCount",
|
|
239
|
+
"MaxChunkConcurrency.$": "$.Payload.MaxChunkConcurrency",
|
|
223
240
|
"Format.$": "$.Payload.Format",
|
|
224
241
|
"HasAudio.$": "$.Payload.HasAudio"
|
|
225
242
|
},
|
|
226
243
|
resultPath: "$.Plan"
|
|
227
244
|
});
|
|
228
245
|
planV2.addRetry({ errors: NON_RETRYABLE_PLAN, maxAttempts: 0 });
|
|
246
|
+
planV2.addRetry(THROTTLED_RETRY);
|
|
229
247
|
planV2.addRetry({
|
|
230
248
|
errors: ["States.ALL"],
|
|
231
249
|
interval: Duration.seconds(2),
|
|
@@ -257,13 +275,16 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
257
275
|
resultSelector: {
|
|
258
276
|
"ChunkS3Uri.$": "$.Payload.ChunkS3Uri",
|
|
259
277
|
"ChunkIndex.$": "$.Payload.ChunkIndex",
|
|
260
|
-
"Sha256.$": "$.Payload.Sha256"
|
|
278
|
+
"Sha256.$": "$.Payload.Sha256",
|
|
279
|
+
"FramesEncoded.$": "$.Payload.FramesEncoded",
|
|
280
|
+
"DurationMs.$": "$.Payload.DurationMs"
|
|
261
281
|
}
|
|
262
282
|
});
|
|
263
283
|
renderChunkTask.addRetry({
|
|
264
284
|
errors: NON_RETRYABLE_CHUNK,
|
|
265
285
|
maxAttempts: 0
|
|
266
286
|
});
|
|
287
|
+
renderChunkTask.addRetry(THROTTLED_RETRY);
|
|
267
288
|
renderChunkTask.addRetry({
|
|
268
289
|
errors: ["States.ALL"],
|
|
269
290
|
interval: Duration.seconds(2),
|
|
@@ -271,7 +292,10 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
271
292
|
backoffRate: 2,
|
|
272
293
|
maxDelay: Duration.seconds(60)
|
|
273
294
|
});
|
|
274
|
-
const renderChunks = new sfn.
|
|
295
|
+
const renderChunks = new sfn.DistributedMap(this, "RenderChunks", {
|
|
296
|
+
label: "RenderChunks",
|
|
297
|
+
// Standard-type children: express ones stop at 5 minutes and a chunk may run the Lambda's full 15.
|
|
298
|
+
mapExecutionType: sfn.StateMachineType.STANDARD,
|
|
275
299
|
itemsPath: "$.Iterator.ChunkIndexes",
|
|
276
300
|
itemSelector: {
|
|
277
301
|
"ChunkIndex.$": "$$.Map.Item.Value",
|
|
@@ -280,7 +304,8 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
280
304
|
"ChunkOutputS3Prefix.$": "$.PlanOutputS3Prefix",
|
|
281
305
|
"Format.$": "$.Plan.Format"
|
|
282
306
|
},
|
|
283
|
-
|
|
307
|
+
// Chunks past the plan's concurrency queue here instead of being throttled by Lambda.
|
|
308
|
+
maxConcurrencyPath: "$.Plan.MaxChunkConcurrency",
|
|
284
309
|
resultPath: "$.Chunks"
|
|
285
310
|
});
|
|
286
311
|
renderChunks.itemProcessor(renderChunkTask);
|
|
@@ -306,6 +331,7 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
306
331
|
errors: NON_RETRYABLE_ASSEMBLE,
|
|
307
332
|
maxAttempts: 0
|
|
308
333
|
});
|
|
334
|
+
assemble.addRetry(THROTTLED_RETRY);
|
|
309
335
|
assemble.addRetry({
|
|
310
336
|
errors: ["States.ALL"],
|
|
311
337
|
interval: Duration.seconds(2),
|
|
@@ -328,10 +354,13 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
328
354
|
resultSelector: {
|
|
329
355
|
"ChunkS3Uri.$": "$.Payload.ChunkS3Uri",
|
|
330
356
|
"ChunkIndex.$": "$.Payload.ChunkIndex",
|
|
331
|
-
"Sha256.$": "$.Payload.Sha256"
|
|
357
|
+
"Sha256.$": "$.Payload.Sha256",
|
|
358
|
+
"FramesEncoded.$": "$.Payload.FramesEncoded",
|
|
359
|
+
"DurationMs.$": "$.Payload.DurationMs"
|
|
332
360
|
}
|
|
333
361
|
});
|
|
334
362
|
renderChunkV2Task.addRetry({ errors: NON_RETRYABLE_CHUNK, maxAttempts: 0 });
|
|
363
|
+
renderChunkV2Task.addRetry(THROTTLED_RETRY);
|
|
335
364
|
renderChunkV2Task.addRetry({
|
|
336
365
|
errors: ["States.ALL"],
|
|
337
366
|
interval: Duration.seconds(2),
|
|
@@ -339,7 +368,9 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
339
368
|
backoffRate: 2,
|
|
340
369
|
maxDelay: Duration.seconds(60)
|
|
341
370
|
});
|
|
342
|
-
const renderChunksV2 = new sfn.
|
|
371
|
+
const renderChunksV2 = new sfn.DistributedMap(this, "RenderChunksV2", {
|
|
372
|
+
label: "RenderChunksV2",
|
|
373
|
+
mapExecutionType: sfn.StateMachineType.STANDARD,
|
|
343
374
|
itemsPath: "$.Iterator.ChunkIndexes",
|
|
344
375
|
itemSelector: {
|
|
345
376
|
"ChunkIndex.$": "$$.Map.Item.Value",
|
|
@@ -349,7 +380,7 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
349
380
|
"ChunkOutputS3Prefix.$": "$.PlanOutputS3Prefix",
|
|
350
381
|
"Format.$": "$.Plan.Format"
|
|
351
382
|
},
|
|
352
|
-
maxConcurrencyPath: "$.Plan.
|
|
383
|
+
maxConcurrencyPath: "$.Plan.MaxChunkConcurrency",
|
|
353
384
|
resultPath: "$.Chunks"
|
|
354
385
|
});
|
|
355
386
|
renderChunksV2.itemProcessor(renderChunkV2Task);
|
|
@@ -374,6 +405,7 @@ var HyperframesRenderStack = class extends Construct {
|
|
|
374
405
|
resultPath: "$.Output"
|
|
375
406
|
});
|
|
376
407
|
assembleV2.addRetry({ errors: NON_RETRYABLE_ASSEMBLE, maxAttempts: 0 });
|
|
408
|
+
assembleV2.addRetry(THROTTLED_RETRY);
|
|
377
409
|
assembleV2.addRetry({
|
|
378
410
|
errors: ["States.ALL"],
|
|
379
411
|
interval: Duration.seconds(2),
|
package/dist/events.d.ts
CHANGED
package/dist/handler.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* primitives. The handler is thin glue: parse event → S3 download → call
|
|
11
11
|
* primitive → S3 upload → return small JSON result.
|
|
12
12
|
*/
|
|
13
|
+
import { LambdaClient } from "@aws-sdk/client-lambda";
|
|
13
14
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
14
15
|
import { assemble, type ChunkRenderer, plan, planV2WithPublisher } from "@hyperframes/producer/distributed";
|
|
15
16
|
import type { AssembleEvent, LambdaEvent, LambdaResult, PlanEvent, RenderChunkEvent } from "./events.js";
|
|
@@ -22,6 +23,8 @@ import type { AssembleEvent, LambdaEvent, LambdaResult, PlanEvent, RenderChunkEv
|
|
|
22
23
|
*/
|
|
23
24
|
export interface HandlerDeps {
|
|
24
25
|
s3?: S3Client;
|
|
26
|
+
/** Reads the account's concurrency when the render does not set one. */
|
|
27
|
+
lambda?: LambdaClient;
|
|
25
28
|
primitives?: {
|
|
26
29
|
plan: typeof plan;
|
|
27
30
|
planV2WithPublisher?: typeof planV2WithPublisher;
|
package/dist/handler.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
|
|
3
3
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4
4
|
import { basename, join as join2 } from "node:path";
|
|
5
|
+
import { GetAccountSettingsCommand, LambdaClient } from "@aws-sdk/client-lambda";
|
|
5
6
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
6
7
|
import {
|
|
7
8
|
assemble,
|
|
9
|
+
chunksAreStitchedMp4,
|
|
10
|
+
DEFAULT_MAX_PARALLEL_CHUNKS,
|
|
8
11
|
listPlanV2ArtifactsForTarget,
|
|
9
12
|
materializePlanV2Target,
|
|
10
13
|
plan,
|
|
@@ -77,16 +80,16 @@ async function loadSparticuzChromium() {
|
|
|
77
80
|
return resolved;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
// src/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
83
|
+
// src/s3Stitch.ts
|
|
84
|
+
import {
|
|
85
|
+
AbortMultipartUploadCommand,
|
|
86
|
+
CompleteMultipartUploadCommand,
|
|
87
|
+
CreateMultipartUploadCommand,
|
|
88
|
+
GetObjectCommand as GetObjectCommand2,
|
|
89
|
+
UploadPartCommand,
|
|
90
|
+
UploadPartCopyCommand
|
|
91
|
+
} from "@aws-sdk/client-s3";
|
|
92
|
+
import { buildStitchedHeader, readMp4Layout } from "@hyperframes/producer/distributed";
|
|
90
93
|
|
|
91
94
|
// src/s3Transport.ts
|
|
92
95
|
import {
|
|
@@ -262,6 +265,135 @@ async function untarDirectory(tarballPath, destDir) {
|
|
|
262
265
|
await tar.extract({ file: tarballPath, cwd: destDir });
|
|
263
266
|
}
|
|
264
267
|
|
|
268
|
+
// src/s3Stitch.ts
|
|
269
|
+
var MIN_PART_BYTES = 5 * 1024 * 1024;
|
|
270
|
+
var segmentSize = (s) => "bytes" in s ? s.bytes.length : s.end - s.start;
|
|
271
|
+
function planParts(segments, min = MIN_PART_BYTES) {
|
|
272
|
+
const parts = [];
|
|
273
|
+
let pending = [];
|
|
274
|
+
let pendingSize = 0;
|
|
275
|
+
const flush = () => {
|
|
276
|
+
if (pending.length) parts.push({ upload: pending });
|
|
277
|
+
pending = [];
|
|
278
|
+
pendingSize = 0;
|
|
279
|
+
};
|
|
280
|
+
for (const segment of segments) {
|
|
281
|
+
const borrow = pendingSize ? min - pendingSize : 0;
|
|
282
|
+
if ("uri" in segment && segmentSize(segment) - borrow >= min) {
|
|
283
|
+
if (borrow) pending.push({ ...segment, end: segment.start + borrow });
|
|
284
|
+
flush();
|
|
285
|
+
parts.push({ copy: { ...segment, start: segment.start + borrow } });
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
pending.push(segment);
|
|
289
|
+
pendingSize += segmentSize(segment);
|
|
290
|
+
if (pendingSize >= min) flush();
|
|
291
|
+
}
|
|
292
|
+
flush();
|
|
293
|
+
return parts;
|
|
294
|
+
}
|
|
295
|
+
async function getRange(s3, uri, start, end) {
|
|
296
|
+
const { bucket, key } = parseS3Uri(uri);
|
|
297
|
+
const response = await s3.send(
|
|
298
|
+
new GetObjectCommand2({ Bucket: bucket, Key: key, Range: `bytes=${start}-${end - 1}` })
|
|
299
|
+
);
|
|
300
|
+
if (!response.Body) throw new Error(`[s3Stitch] empty body for ${uri}`);
|
|
301
|
+
return Buffer.from(await response.Body.transformToByteArray());
|
|
302
|
+
}
|
|
303
|
+
async function sendPart(s3, part, partNumber, target) {
|
|
304
|
+
if ("copy" in part) {
|
|
305
|
+
const { uri, start, end } = part.copy;
|
|
306
|
+
const source = parseS3Uri(uri);
|
|
307
|
+
const result2 = await s3.send(
|
|
308
|
+
new UploadPartCopyCommand({
|
|
309
|
+
...target,
|
|
310
|
+
PartNumber: partNumber,
|
|
311
|
+
CopySource: encodeURI(`${source.bucket}/${source.key}`),
|
|
312
|
+
CopySourceRange: `bytes=${start}-${end - 1}`
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
return result2.CopyPartResult?.ETag;
|
|
316
|
+
}
|
|
317
|
+
const body = await Promise.all(
|
|
318
|
+
part.upload.map((s) => "bytes" in s ? s.bytes : getRange(s3, s.uri, s.start, s.end))
|
|
319
|
+
);
|
|
320
|
+
const result = await s3.send(
|
|
321
|
+
new UploadPartCommand({ ...target, PartNumber: partNumber, Body: Buffer.concat(body) })
|
|
322
|
+
);
|
|
323
|
+
return result.ETag;
|
|
324
|
+
}
|
|
325
|
+
async function sendParts(s3, parts, target) {
|
|
326
|
+
const settled = await Promise.allSettled(
|
|
327
|
+
parts.map(async (part, index) => {
|
|
328
|
+
const etag = await sendPart(s3, part, index + 1, target);
|
|
329
|
+
if (!etag) throw new Error(`[s3Stitch] part ${index + 1} returned no ETag`);
|
|
330
|
+
return etag;
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
const failure = settled.find(
|
|
334
|
+
(result) => result.status === "rejected"
|
|
335
|
+
);
|
|
336
|
+
if (failure) throw failure.reason;
|
|
337
|
+
return settled.map((result) => result.value);
|
|
338
|
+
}
|
|
339
|
+
async function uploadParts(s3, parts, destUri, contentType) {
|
|
340
|
+
const { bucket, key } = parseS3Uri(destUri);
|
|
341
|
+
const created = await s3.send(
|
|
342
|
+
new CreateMultipartUploadCommand({ Bucket: bucket, Key: key, ContentType: contentType })
|
|
343
|
+
);
|
|
344
|
+
if (!created.UploadId) throw new Error(`[s3Stitch] no upload id for ${destUri}`);
|
|
345
|
+
const target = { Bucket: bucket, Key: key, UploadId: created.UploadId };
|
|
346
|
+
try {
|
|
347
|
+
const etags = await sendParts(s3, parts, target);
|
|
348
|
+
await s3.send(
|
|
349
|
+
new CompleteMultipartUploadCommand({
|
|
350
|
+
...target,
|
|
351
|
+
MultipartUpload: { Parts: etags.map((ETag, index) => ({ ETag, PartNumber: index + 1 })) }
|
|
352
|
+
})
|
|
353
|
+
);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
await s3.send(new AbortMultipartUploadCommand(target)).catch((abortError) => {
|
|
356
|
+
console.error(
|
|
357
|
+
`[s3Stitch] abort failed for ${destUri}; parts remain until a lifecycle rule removes them`,
|
|
358
|
+
abortError
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function stitchMp4InS3(s3, chunkUris, destUri, minPartBytes = MIN_PART_BYTES) {
|
|
365
|
+
const layouts = await Promise.all(
|
|
366
|
+
chunkUris.map((uri) => readMp4Layout((start, end) => getRange(s3, uri, start, end)))
|
|
367
|
+
);
|
|
368
|
+
const segments = [
|
|
369
|
+
{ bytes: buildStitchedHeader(layouts) },
|
|
370
|
+
...layouts.map((layout, i) => ({
|
|
371
|
+
uri: chunkUris[i],
|
|
372
|
+
start: layout.payloadStart,
|
|
373
|
+
end: layout.payloadEnd
|
|
374
|
+
}))
|
|
375
|
+
];
|
|
376
|
+
const parts = planParts(segments, minPartBytes);
|
|
377
|
+
await uploadParts(s3, parts, destUri, "video/mp4");
|
|
378
|
+
const uploaded = parts.flatMap((part) => "upload" in part ? part.upload : []);
|
|
379
|
+
return {
|
|
380
|
+
fileSize: segments.reduce((total, s) => total + segmentSize(s), 0),
|
|
381
|
+
parts: parts.length,
|
|
382
|
+
downloadedBytes: uploaded.reduce((total, s) => total + ("uri" in s ? segmentSize(s) : 0), 0)
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/formatExtension.ts
|
|
387
|
+
var FORMAT_EXTENSIONS = {
|
|
388
|
+
mp4: ".mp4",
|
|
389
|
+
mov: ".mov",
|
|
390
|
+
webm: ".webm",
|
|
391
|
+
"png-sequence": ""
|
|
392
|
+
};
|
|
393
|
+
function formatExtension(format) {
|
|
394
|
+
return FORMAT_EXTENSIONS[format];
|
|
395
|
+
}
|
|
396
|
+
|
|
265
397
|
// src/s3PlanV2Publisher.ts
|
|
266
398
|
import { createHash as createHash2 } from "node:crypto";
|
|
267
399
|
import { mkdirSync as mkdirSync2, mkdtempSync, rmSync as rmSync2, statSync as statSync2, writeFileSync } from "node:fs";
|
|
@@ -515,19 +647,49 @@ function primeRuntimeEnv() {
|
|
|
515
647
|
process.env.PATH = `${bin}:${process.env.PATH ?? ""}`;
|
|
516
648
|
}
|
|
517
649
|
}
|
|
518
|
-
|
|
650
|
+
var ACCOUNT_CONCURRENCY_SHARE = 0.9;
|
|
651
|
+
async function resolveWorkerConcurrency(event, deps) {
|
|
652
|
+
if (event.Config.workerConcurrency !== void 0) return event.Config.workerConcurrency;
|
|
653
|
+
try {
|
|
654
|
+
return await functionConcurrency(deps?.lambda);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
logEvent({
|
|
657
|
+
event: "worker_concurrency",
|
|
658
|
+
level: "WARN",
|
|
659
|
+
source: "fallback",
|
|
660
|
+
concurrency: DEFAULT_MAX_PARALLEL_CHUNKS,
|
|
661
|
+
message: `Could not read the account's Lambda concurrency (${String(err)}); planning for ${DEFAULT_MAX_PARALLEL_CHUNKS} chunks at once. Allow lambda:GetAccountSettings on the function role, or set the concurrency explicitly.`
|
|
662
|
+
});
|
|
663
|
+
return DEFAULT_MAX_PARALLEL_CHUNKS;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function functionConcurrency(lambda = new LambdaClient({})) {
|
|
667
|
+
const reserved = Number(process.env.HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY);
|
|
668
|
+
if (reserved > 0) return reserved;
|
|
669
|
+
const { AccountLimit } = await lambda.send(new GetAccountSettingsCommand({}));
|
|
670
|
+
const unreserved = AccountLimit?.UnreservedConcurrentExecutions;
|
|
671
|
+
if (unreserved === void 0) throw new Error("no UnreservedConcurrentExecutions in the reply");
|
|
672
|
+
const concurrency = Math.max(1, Math.floor(unreserved * ACCOUNT_CONCURRENCY_SHARE));
|
|
673
|
+
logEvent({ event: "worker_concurrency", source: "account", unreserved, concurrency });
|
|
674
|
+
return concurrency;
|
|
675
|
+
}
|
|
676
|
+
async function planConfig(event, deps) {
|
|
677
|
+
const config = {
|
|
678
|
+
...event.Config,
|
|
679
|
+
workerConcurrency: await resolveWorkerConcurrency(event, deps)
|
|
680
|
+
};
|
|
519
681
|
const remainingMs = deps?.getRemainingTimeInMillis?.();
|
|
520
|
-
if (
|
|
521
|
-
|
|
682
|
+
if (config.workerTimeoutSeconds === void 0 && remainingMs !== void 0) {
|
|
683
|
+
config.workerTimeoutSeconds = Math.ceil(remainingMs / 1e3);
|
|
522
684
|
}
|
|
523
|
-
return
|
|
685
|
+
return config;
|
|
524
686
|
}
|
|
525
687
|
async function handlePlan(event, deps) {
|
|
526
688
|
if (event.PlanProtocol !== "v1") {
|
|
527
689
|
return handlePlanV2(event, deps);
|
|
528
690
|
}
|
|
529
691
|
const started = Date.now();
|
|
530
|
-
const config = planConfig(event, deps);
|
|
692
|
+
const config = await planConfig(event, deps);
|
|
531
693
|
const s3 = deps?.s3 ?? getS3Client();
|
|
532
694
|
const primitive = deps?.primitives?.plan ?? plan;
|
|
533
695
|
if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
|
@@ -541,7 +703,9 @@ async function handlePlan(event, deps) {
|
|
|
541
703
|
try {
|
|
542
704
|
await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive);
|
|
543
705
|
await untarDirectory(projectArchive, projectDir);
|
|
544
|
-
const result = await primitive(projectDir, config, planDir
|
|
706
|
+
const result = await primitive(projectDir, config, planDir, {
|
|
707
|
+
chunksCarryAudio: true
|
|
708
|
+
});
|
|
545
709
|
const planTar = join2(work, "plan.tar.gz");
|
|
546
710
|
await tarDirectory(planDir, planTar);
|
|
547
711
|
const planTarUri = `${trimTrailingSlash2(event.PlanOutputS3Prefix)}/plan.tar.gz`;
|
|
@@ -557,6 +721,7 @@ async function handlePlan(event, deps) {
|
|
|
557
721
|
PlanS3Uri: planTarUri,
|
|
558
722
|
PlanHash: result.planHash,
|
|
559
723
|
ChunkCount: result.chunkCount,
|
|
724
|
+
MaxChunkConcurrency: Math.min(config.workerConcurrency, result.chunkCount),
|
|
560
725
|
TotalFrames: result.totalFrames,
|
|
561
726
|
Fps: result.fps,
|
|
562
727
|
Width: result.width,
|
|
@@ -574,7 +739,7 @@ async function handlePlan(event, deps) {
|
|
|
574
739
|
}
|
|
575
740
|
async function handlePlanV2(event, deps) {
|
|
576
741
|
const started = Date.now();
|
|
577
|
-
const config = planConfig(event, deps);
|
|
742
|
+
const config = await planConfig(event, deps);
|
|
578
743
|
const s3 = deps?.s3 ?? getS3Client();
|
|
579
744
|
const primitive = deps?.primitives?.planV2WithPublisher ?? planV2WithPublisher;
|
|
580
745
|
if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
|
@@ -596,7 +761,8 @@ async function handlePlanV2(event, deps) {
|
|
|
596
761
|
temporaryRoot: work
|
|
597
762
|
});
|
|
598
763
|
const manifest = await primitive(projectDir, config, publisher, {
|
|
599
|
-
stagingParentDir: work
|
|
764
|
+
stagingParentDir: work,
|
|
765
|
+
chunksCarryAudio: true
|
|
600
766
|
});
|
|
601
767
|
return {
|
|
602
768
|
Action: "plan",
|
|
@@ -605,6 +771,7 @@ async function handlePlanV2(event, deps) {
|
|
|
605
771
|
PlanV2ArtifactS3Prefix: publisher.artifactPrefix,
|
|
606
772
|
PlanHash: manifest.planHash,
|
|
607
773
|
ChunkCount: manifest.chunkCount,
|
|
774
|
+
MaxChunkConcurrency: Math.min(config.workerConcurrency, manifest.chunkCount),
|
|
608
775
|
TotalFrames: manifest.totalFrames,
|
|
609
776
|
Fps: manifest.fps,
|
|
610
777
|
Width: manifest.width,
|
|
@@ -671,7 +838,7 @@ async function handleRenderChunkV2(event, deps) {
|
|
|
671
838
|
}
|
|
672
839
|
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-chunk-v2-"));
|
|
673
840
|
try {
|
|
674
|
-
const planDir = await downloadAndMaterializePlanV2(
|
|
841
|
+
const { planDir } = await downloadAndMaterializePlanV2(
|
|
675
842
|
s3,
|
|
676
843
|
event,
|
|
677
844
|
{ role: "chunk", chunkIndex: event.ChunkIndex },
|
|
@@ -762,7 +929,15 @@ async function handleAssembleV2(event, deps) {
|
|
|
762
929
|
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
763
930
|
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-assemble-v2-"));
|
|
764
931
|
try {
|
|
765
|
-
const planDir = await downloadAndMaterializePlanV2(
|
|
932
|
+
const { planDir, manifest } = await downloadAndMaterializePlanV2(
|
|
933
|
+
s3,
|
|
934
|
+
event,
|
|
935
|
+
{ role: "assembler" },
|
|
936
|
+
work
|
|
937
|
+
);
|
|
938
|
+
if (chunksAreStitchedMp4(planDir, event.Cfr === true)) {
|
|
939
|
+
return await stitchChunksInS3(s3, event, manifest, started);
|
|
940
|
+
}
|
|
766
941
|
const audioPath = resolvePlanAudioPath(planDir);
|
|
767
942
|
const chunksStarted = Date.now();
|
|
768
943
|
const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
|
|
@@ -797,6 +972,30 @@ async function handleAssembleV2(event, deps) {
|
|
|
797
972
|
cleanupDir(work);
|
|
798
973
|
}
|
|
799
974
|
}
|
|
975
|
+
async function stitchChunksInS3(s3, event, plan2, started) {
|
|
976
|
+
if (event.ChunkS3Uris.length !== plan2.chunkCount) {
|
|
977
|
+
throw new Error(
|
|
978
|
+
`[handler] ${event.ChunkS3Uris.length} chunk URIs for a plan of ${plan2.chunkCount} chunks`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
const stitchStarted = Date.now();
|
|
982
|
+
const stitched = await stitchMp4InS3(s3, event.ChunkS3Uris, event.OutputS3Uri);
|
|
983
|
+
logEvent({
|
|
984
|
+
event: "assemble_stages",
|
|
985
|
+
mode: "s3-stitch",
|
|
986
|
+
planMs: stitchStarted - started,
|
|
987
|
+
stitchMs: Date.now() - stitchStarted,
|
|
988
|
+
parts: stitched.parts,
|
|
989
|
+
downloadedBytes: stitched.downloadedBytes
|
|
990
|
+
});
|
|
991
|
+
return {
|
|
992
|
+
Action: "assemble",
|
|
993
|
+
OutputS3Uri: event.OutputS3Uri,
|
|
994
|
+
FramesEncoded: plan2.totalFrames,
|
|
995
|
+
FileSize: stitched.fileSize,
|
|
996
|
+
DurationMs: Date.now() - started
|
|
997
|
+
};
|
|
998
|
+
}
|
|
800
999
|
async function downloadAndMaterializePlanV2(s3, event, target, work) {
|
|
801
1000
|
const transportDir = join2(work, "plan-v2");
|
|
802
1001
|
mkdirSync3(transportDir, { recursive: true });
|
|
@@ -814,7 +1013,7 @@ async function downloadAndMaterializePlanV2(s3, event, target, work) {
|
|
|
814
1013
|
});
|
|
815
1014
|
const planDir = join2(work, "plan");
|
|
816
1015
|
materializePlanV2Target(transportDir, target, planDir);
|
|
817
|
-
return planDir;
|
|
1016
|
+
return { planDir, manifest };
|
|
818
1017
|
}
|
|
819
1018
|
async function downloadPlanV2Artifact(s3, artifactPrefix, planV2Dir, artifact) {
|
|
820
1019
|
await downloadS3ObjectToFileVerified(
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
|
|
3
3
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4
4
|
import { basename, join as join2 } from "node:path";
|
|
5
|
+
import { GetAccountSettingsCommand, LambdaClient } from "@aws-sdk/client-lambda";
|
|
5
6
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
6
7
|
import {
|
|
7
8
|
assemble,
|
|
9
|
+
chunksAreStitchedMp4,
|
|
10
|
+
DEFAULT_MAX_PARALLEL_CHUNKS,
|
|
8
11
|
listPlanV2ArtifactsForTarget,
|
|
9
12
|
materializePlanV2Target,
|
|
10
13
|
plan,
|
|
@@ -82,16 +85,16 @@ async function loadSparticuzChromium() {
|
|
|
82
85
|
return resolved;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
// src/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
88
|
+
// src/s3Stitch.ts
|
|
89
|
+
import {
|
|
90
|
+
AbortMultipartUploadCommand,
|
|
91
|
+
CompleteMultipartUploadCommand,
|
|
92
|
+
CreateMultipartUploadCommand,
|
|
93
|
+
GetObjectCommand as GetObjectCommand2,
|
|
94
|
+
UploadPartCommand,
|
|
95
|
+
UploadPartCopyCommand
|
|
96
|
+
} from "@aws-sdk/client-s3";
|
|
97
|
+
import { buildStitchedHeader, readMp4Layout } from "@hyperframes/producer/distributed";
|
|
95
98
|
|
|
96
99
|
// src/s3Transport.ts
|
|
97
100
|
import {
|
|
@@ -270,6 +273,135 @@ async function untarDirectory(tarballPath, destDir) {
|
|
|
270
273
|
await tar.extract({ file: tarballPath, cwd: destDir });
|
|
271
274
|
}
|
|
272
275
|
|
|
276
|
+
// src/s3Stitch.ts
|
|
277
|
+
var MIN_PART_BYTES = 5 * 1024 * 1024;
|
|
278
|
+
var segmentSize = (s) => "bytes" in s ? s.bytes.length : s.end - s.start;
|
|
279
|
+
function planParts(segments, min = MIN_PART_BYTES) {
|
|
280
|
+
const parts = [];
|
|
281
|
+
let pending = [];
|
|
282
|
+
let pendingSize = 0;
|
|
283
|
+
const flush = () => {
|
|
284
|
+
if (pending.length) parts.push({ upload: pending });
|
|
285
|
+
pending = [];
|
|
286
|
+
pendingSize = 0;
|
|
287
|
+
};
|
|
288
|
+
for (const segment of segments) {
|
|
289
|
+
const borrow = pendingSize ? min - pendingSize : 0;
|
|
290
|
+
if ("uri" in segment && segmentSize(segment) - borrow >= min) {
|
|
291
|
+
if (borrow) pending.push({ ...segment, end: segment.start + borrow });
|
|
292
|
+
flush();
|
|
293
|
+
parts.push({ copy: { ...segment, start: segment.start + borrow } });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
pending.push(segment);
|
|
297
|
+
pendingSize += segmentSize(segment);
|
|
298
|
+
if (pendingSize >= min) flush();
|
|
299
|
+
}
|
|
300
|
+
flush();
|
|
301
|
+
return parts;
|
|
302
|
+
}
|
|
303
|
+
async function getRange(s3, uri, start, end) {
|
|
304
|
+
const { bucket, key } = parseS3Uri(uri);
|
|
305
|
+
const response = await s3.send(
|
|
306
|
+
new GetObjectCommand2({ Bucket: bucket, Key: key, Range: `bytes=${start}-${end - 1}` })
|
|
307
|
+
);
|
|
308
|
+
if (!response.Body) throw new Error(`[s3Stitch] empty body for ${uri}`);
|
|
309
|
+
return Buffer.from(await response.Body.transformToByteArray());
|
|
310
|
+
}
|
|
311
|
+
async function sendPart(s3, part, partNumber, target) {
|
|
312
|
+
if ("copy" in part) {
|
|
313
|
+
const { uri, start, end } = part.copy;
|
|
314
|
+
const source = parseS3Uri(uri);
|
|
315
|
+
const result2 = await s3.send(
|
|
316
|
+
new UploadPartCopyCommand({
|
|
317
|
+
...target,
|
|
318
|
+
PartNumber: partNumber,
|
|
319
|
+
CopySource: encodeURI(`${source.bucket}/${source.key}`),
|
|
320
|
+
CopySourceRange: `bytes=${start}-${end - 1}`
|
|
321
|
+
})
|
|
322
|
+
);
|
|
323
|
+
return result2.CopyPartResult?.ETag;
|
|
324
|
+
}
|
|
325
|
+
const body = await Promise.all(
|
|
326
|
+
part.upload.map((s) => "bytes" in s ? s.bytes : getRange(s3, s.uri, s.start, s.end))
|
|
327
|
+
);
|
|
328
|
+
const result = await s3.send(
|
|
329
|
+
new UploadPartCommand({ ...target, PartNumber: partNumber, Body: Buffer.concat(body) })
|
|
330
|
+
);
|
|
331
|
+
return result.ETag;
|
|
332
|
+
}
|
|
333
|
+
async function sendParts(s3, parts, target) {
|
|
334
|
+
const settled = await Promise.allSettled(
|
|
335
|
+
parts.map(async (part, index) => {
|
|
336
|
+
const etag = await sendPart(s3, part, index + 1, target);
|
|
337
|
+
if (!etag) throw new Error(`[s3Stitch] part ${index + 1} returned no ETag`);
|
|
338
|
+
return etag;
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
const failure = settled.find(
|
|
342
|
+
(result) => result.status === "rejected"
|
|
343
|
+
);
|
|
344
|
+
if (failure) throw failure.reason;
|
|
345
|
+
return settled.map((result) => result.value);
|
|
346
|
+
}
|
|
347
|
+
async function uploadParts(s3, parts, destUri, contentType) {
|
|
348
|
+
const { bucket, key } = parseS3Uri(destUri);
|
|
349
|
+
const created = await s3.send(
|
|
350
|
+
new CreateMultipartUploadCommand({ Bucket: bucket, Key: key, ContentType: contentType })
|
|
351
|
+
);
|
|
352
|
+
if (!created.UploadId) throw new Error(`[s3Stitch] no upload id for ${destUri}`);
|
|
353
|
+
const target = { Bucket: bucket, Key: key, UploadId: created.UploadId };
|
|
354
|
+
try {
|
|
355
|
+
const etags = await sendParts(s3, parts, target);
|
|
356
|
+
await s3.send(
|
|
357
|
+
new CompleteMultipartUploadCommand({
|
|
358
|
+
...target,
|
|
359
|
+
MultipartUpload: { Parts: etags.map((ETag, index) => ({ ETag, PartNumber: index + 1 })) }
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
await s3.send(new AbortMultipartUploadCommand(target)).catch((abortError) => {
|
|
364
|
+
console.error(
|
|
365
|
+
`[s3Stitch] abort failed for ${destUri}; parts remain until a lifecycle rule removes them`,
|
|
366
|
+
abortError
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function stitchMp4InS3(s3, chunkUris, destUri, minPartBytes = MIN_PART_BYTES) {
|
|
373
|
+
const layouts = await Promise.all(
|
|
374
|
+
chunkUris.map((uri) => readMp4Layout((start, end) => getRange(s3, uri, start, end)))
|
|
375
|
+
);
|
|
376
|
+
const segments = [
|
|
377
|
+
{ bytes: buildStitchedHeader(layouts) },
|
|
378
|
+
...layouts.map((layout, i) => ({
|
|
379
|
+
uri: chunkUris[i],
|
|
380
|
+
start: layout.payloadStart,
|
|
381
|
+
end: layout.payloadEnd
|
|
382
|
+
}))
|
|
383
|
+
];
|
|
384
|
+
const parts = planParts(segments, minPartBytes);
|
|
385
|
+
await uploadParts(s3, parts, destUri, "video/mp4");
|
|
386
|
+
const uploaded = parts.flatMap((part) => "upload" in part ? part.upload : []);
|
|
387
|
+
return {
|
|
388
|
+
fileSize: segments.reduce((total, s) => total + segmentSize(s), 0),
|
|
389
|
+
parts: parts.length,
|
|
390
|
+
downloadedBytes: uploaded.reduce((total, s) => total + ("uri" in s ? segmentSize(s) : 0), 0)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/formatExtension.ts
|
|
395
|
+
var FORMAT_EXTENSIONS = {
|
|
396
|
+
mp4: ".mp4",
|
|
397
|
+
mov: ".mov",
|
|
398
|
+
webm: ".webm",
|
|
399
|
+
"png-sequence": ""
|
|
400
|
+
};
|
|
401
|
+
function formatExtension(format) {
|
|
402
|
+
return FORMAT_EXTENSIONS[format];
|
|
403
|
+
}
|
|
404
|
+
|
|
273
405
|
// src/s3PlanV2Publisher.ts
|
|
274
406
|
import { createHash as createHash2 } from "node:crypto";
|
|
275
407
|
import { mkdirSync as mkdirSync2, mkdtempSync, rmSync as rmSync2, statSync as statSync2, writeFileSync } from "node:fs";
|
|
@@ -523,19 +655,49 @@ function primeRuntimeEnv() {
|
|
|
523
655
|
process.env.PATH = `${bin}:${process.env.PATH ?? ""}`;
|
|
524
656
|
}
|
|
525
657
|
}
|
|
526
|
-
|
|
658
|
+
var ACCOUNT_CONCURRENCY_SHARE = 0.9;
|
|
659
|
+
async function resolveWorkerConcurrency(event, deps) {
|
|
660
|
+
if (event.Config.workerConcurrency !== void 0) return event.Config.workerConcurrency;
|
|
661
|
+
try {
|
|
662
|
+
return await functionConcurrency(deps?.lambda);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
logEvent({
|
|
665
|
+
event: "worker_concurrency",
|
|
666
|
+
level: "WARN",
|
|
667
|
+
source: "fallback",
|
|
668
|
+
concurrency: DEFAULT_MAX_PARALLEL_CHUNKS,
|
|
669
|
+
message: `Could not read the account's Lambda concurrency (${String(err)}); planning for ${DEFAULT_MAX_PARALLEL_CHUNKS} chunks at once. Allow lambda:GetAccountSettings on the function role, or set the concurrency explicitly.`
|
|
670
|
+
});
|
|
671
|
+
return DEFAULT_MAX_PARALLEL_CHUNKS;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
async function functionConcurrency(lambda = new LambdaClient({})) {
|
|
675
|
+
const reserved = Number(process.env.HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY);
|
|
676
|
+
if (reserved > 0) return reserved;
|
|
677
|
+
const { AccountLimit } = await lambda.send(new GetAccountSettingsCommand({}));
|
|
678
|
+
const unreserved = AccountLimit?.UnreservedConcurrentExecutions;
|
|
679
|
+
if (unreserved === void 0) throw new Error("no UnreservedConcurrentExecutions in the reply");
|
|
680
|
+
const concurrency = Math.max(1, Math.floor(unreserved * ACCOUNT_CONCURRENCY_SHARE));
|
|
681
|
+
logEvent({ event: "worker_concurrency", source: "account", unreserved, concurrency });
|
|
682
|
+
return concurrency;
|
|
683
|
+
}
|
|
684
|
+
async function planConfig(event, deps) {
|
|
685
|
+
const config = {
|
|
686
|
+
...event.Config,
|
|
687
|
+
workerConcurrency: await resolveWorkerConcurrency(event, deps)
|
|
688
|
+
};
|
|
527
689
|
const remainingMs = deps?.getRemainingTimeInMillis?.();
|
|
528
|
-
if (
|
|
529
|
-
|
|
690
|
+
if (config.workerTimeoutSeconds === void 0 && remainingMs !== void 0) {
|
|
691
|
+
config.workerTimeoutSeconds = Math.ceil(remainingMs / 1e3);
|
|
530
692
|
}
|
|
531
|
-
return
|
|
693
|
+
return config;
|
|
532
694
|
}
|
|
533
695
|
async function handlePlan(event, deps) {
|
|
534
696
|
if (event.PlanProtocol !== "v1") {
|
|
535
697
|
return handlePlanV2(event, deps);
|
|
536
698
|
}
|
|
537
699
|
const started = Date.now();
|
|
538
|
-
const config = planConfig(event, deps);
|
|
700
|
+
const config = await planConfig(event, deps);
|
|
539
701
|
const s3 = deps?.s3 ?? getS3Client();
|
|
540
702
|
const primitive = deps?.primitives?.plan ?? plan;
|
|
541
703
|
if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
|
@@ -549,7 +711,9 @@ async function handlePlan(event, deps) {
|
|
|
549
711
|
try {
|
|
550
712
|
await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive);
|
|
551
713
|
await untarDirectory(projectArchive, projectDir);
|
|
552
|
-
const result = await primitive(projectDir, config, planDir
|
|
714
|
+
const result = await primitive(projectDir, config, planDir, {
|
|
715
|
+
chunksCarryAudio: true
|
|
716
|
+
});
|
|
553
717
|
const planTar = join2(work, "plan.tar.gz");
|
|
554
718
|
await tarDirectory(planDir, planTar);
|
|
555
719
|
const planTarUri = `${trimTrailingSlash2(event.PlanOutputS3Prefix)}/plan.tar.gz`;
|
|
@@ -565,6 +729,7 @@ async function handlePlan(event, deps) {
|
|
|
565
729
|
PlanS3Uri: planTarUri,
|
|
566
730
|
PlanHash: result.planHash,
|
|
567
731
|
ChunkCount: result.chunkCount,
|
|
732
|
+
MaxChunkConcurrency: Math.min(config.workerConcurrency, result.chunkCount),
|
|
568
733
|
TotalFrames: result.totalFrames,
|
|
569
734
|
Fps: result.fps,
|
|
570
735
|
Width: result.width,
|
|
@@ -582,7 +747,7 @@ async function handlePlan(event, deps) {
|
|
|
582
747
|
}
|
|
583
748
|
async function handlePlanV2(event, deps) {
|
|
584
749
|
const started = Date.now();
|
|
585
|
-
const config = planConfig(event, deps);
|
|
750
|
+
const config = await planConfig(event, deps);
|
|
586
751
|
const s3 = deps?.s3 ?? getS3Client();
|
|
587
752
|
const primitive = deps?.primitives?.planV2WithPublisher ?? planV2WithPublisher;
|
|
588
753
|
if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
|
@@ -604,7 +769,8 @@ async function handlePlanV2(event, deps) {
|
|
|
604
769
|
temporaryRoot: work
|
|
605
770
|
});
|
|
606
771
|
const manifest = await primitive(projectDir, config, publisher, {
|
|
607
|
-
stagingParentDir: work
|
|
772
|
+
stagingParentDir: work,
|
|
773
|
+
chunksCarryAudio: true
|
|
608
774
|
});
|
|
609
775
|
return {
|
|
610
776
|
Action: "plan",
|
|
@@ -613,6 +779,7 @@ async function handlePlanV2(event, deps) {
|
|
|
613
779
|
PlanV2ArtifactS3Prefix: publisher.artifactPrefix,
|
|
614
780
|
PlanHash: manifest.planHash,
|
|
615
781
|
ChunkCount: manifest.chunkCount,
|
|
782
|
+
MaxChunkConcurrency: Math.min(config.workerConcurrency, manifest.chunkCount),
|
|
616
783
|
TotalFrames: manifest.totalFrames,
|
|
617
784
|
Fps: manifest.fps,
|
|
618
785
|
Width: manifest.width,
|
|
@@ -679,7 +846,7 @@ async function handleRenderChunkV2(event, deps) {
|
|
|
679
846
|
}
|
|
680
847
|
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-chunk-v2-"));
|
|
681
848
|
try {
|
|
682
|
-
const planDir = await downloadAndMaterializePlanV2(
|
|
849
|
+
const { planDir } = await downloadAndMaterializePlanV2(
|
|
683
850
|
s3,
|
|
684
851
|
event,
|
|
685
852
|
{ role: "chunk", chunkIndex: event.ChunkIndex },
|
|
@@ -770,7 +937,15 @@ async function handleAssembleV2(event, deps) {
|
|
|
770
937
|
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
771
938
|
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-assemble-v2-"));
|
|
772
939
|
try {
|
|
773
|
-
const planDir = await downloadAndMaterializePlanV2(
|
|
940
|
+
const { planDir, manifest } = await downloadAndMaterializePlanV2(
|
|
941
|
+
s3,
|
|
942
|
+
event,
|
|
943
|
+
{ role: "assembler" },
|
|
944
|
+
work
|
|
945
|
+
);
|
|
946
|
+
if (chunksAreStitchedMp4(planDir, event.Cfr === true)) {
|
|
947
|
+
return await stitchChunksInS3(s3, event, manifest, started);
|
|
948
|
+
}
|
|
774
949
|
const audioPath = resolvePlanAudioPath(planDir);
|
|
775
950
|
const chunksStarted = Date.now();
|
|
776
951
|
const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
|
|
@@ -805,6 +980,30 @@ async function handleAssembleV2(event, deps) {
|
|
|
805
980
|
cleanupDir(work);
|
|
806
981
|
}
|
|
807
982
|
}
|
|
983
|
+
async function stitchChunksInS3(s3, event, plan2, started) {
|
|
984
|
+
if (event.ChunkS3Uris.length !== plan2.chunkCount) {
|
|
985
|
+
throw new Error(
|
|
986
|
+
`[handler] ${event.ChunkS3Uris.length} chunk URIs for a plan of ${plan2.chunkCount} chunks`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const stitchStarted = Date.now();
|
|
990
|
+
const stitched = await stitchMp4InS3(s3, event.ChunkS3Uris, event.OutputS3Uri);
|
|
991
|
+
logEvent({
|
|
992
|
+
event: "assemble_stages",
|
|
993
|
+
mode: "s3-stitch",
|
|
994
|
+
planMs: stitchStarted - started,
|
|
995
|
+
stitchMs: Date.now() - stitchStarted,
|
|
996
|
+
parts: stitched.parts,
|
|
997
|
+
downloadedBytes: stitched.downloadedBytes
|
|
998
|
+
});
|
|
999
|
+
return {
|
|
1000
|
+
Action: "assemble",
|
|
1001
|
+
OutputS3Uri: event.OutputS3Uri,
|
|
1002
|
+
FramesEncoded: plan2.totalFrames,
|
|
1003
|
+
FileSize: stitched.fileSize,
|
|
1004
|
+
DurationMs: Date.now() - started
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
808
1007
|
async function downloadAndMaterializePlanV2(s3, event, target, work) {
|
|
809
1008
|
const transportDir = join2(work, "plan-v2");
|
|
810
1009
|
mkdirSync3(transportDir, { recursive: true });
|
|
@@ -822,7 +1021,7 @@ async function downloadAndMaterializePlanV2(s3, event, target, work) {
|
|
|
822
1021
|
});
|
|
823
1022
|
const planDir = join2(work, "plan");
|
|
824
1023
|
materializePlanV2Target(transportDir, target, planDir);
|
|
825
|
-
return planDir;
|
|
1024
|
+
return { planDir, manifest };
|
|
826
1025
|
}
|
|
827
1026
|
async function downloadPlanV2Artifact(s3, artifactPrefix, planV2Dir, artifact) {
|
|
828
1027
|
await downloadS3ObjectToFileVerified(
|
|
@@ -1103,7 +1302,9 @@ async function renderToLambda(opts) {
|
|
|
1103
1302
|
// src/sdk/getRenderProgress.ts
|
|
1104
1303
|
import {
|
|
1105
1304
|
DescribeExecutionCommand,
|
|
1305
|
+
DescribeMapRunCommand,
|
|
1106
1306
|
GetExecutionHistoryCommand,
|
|
1307
|
+
ListExecutionsCommand,
|
|
1107
1308
|
SFNClient as SFNClient2
|
|
1108
1309
|
} from "@aws-sdk/client-sfn";
|
|
1109
1310
|
|
|
@@ -1140,11 +1341,14 @@ function formatUsd(usd) {
|
|
|
1140
1341
|
|
|
1141
1342
|
// src/sdk/getRenderProgress.ts
|
|
1142
1343
|
var DEFAULT_MEMORY_MB = 10240;
|
|
1344
|
+
function createProgressSfnClient(config = {}) {
|
|
1345
|
+
return new SFNClient2({ maxAttempts: 8, ...config });
|
|
1346
|
+
}
|
|
1143
1347
|
async function getRenderProgress(opts) {
|
|
1144
1348
|
if (!opts.executionArn) {
|
|
1145
1349
|
throw new Error("[getRenderProgress] executionArn is required");
|
|
1146
1350
|
}
|
|
1147
|
-
const sfn = opts.sfn ??
|
|
1351
|
+
const sfn = opts.sfn ?? createProgressSfnClient({ region: opts.region });
|
|
1148
1352
|
const memoryMb = opts.defaultMemorySizeMb ?? DEFAULT_MEMORY_MB;
|
|
1149
1353
|
const describe = await sfn.send(
|
|
1150
1354
|
new DescribeExecutionCommand({ executionArn: opts.executionArn })
|
|
@@ -1154,6 +1358,15 @@ async function getRenderProgress(opts) {
|
|
|
1154
1358
|
const endedAt = describe.stopDate?.toISOString() ?? null;
|
|
1155
1359
|
const history = await loadFullHistory(sfn, opts.executionArn);
|
|
1156
1360
|
const summary = summarizeHistory(history, memoryMb);
|
|
1361
|
+
const mapRun = findChunkMapRun(history);
|
|
1362
|
+
const warnings = [];
|
|
1363
|
+
if (mapRun?.exited) addChunkResults(summary, mapRun.output, memoryMb);
|
|
1364
|
+
else if (mapRun) {
|
|
1365
|
+
await addRunningMapRun(sfn, mapRun.arn, summary, memoryMb).catch((err) => {
|
|
1366
|
+
if (err?.name !== "AccessDeniedException") throw err;
|
|
1367
|
+
warnings.push(MAP_RUN_ACCESS_DENIED);
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1157
1370
|
const costs = computeRenderCost(summary.lambdaInvocations, summary.stateTransitions);
|
|
1158
1371
|
const overallProgress = computeOverallProgress({
|
|
1159
1372
|
status,
|
|
@@ -1171,6 +1384,7 @@ async function getRenderProgress(opts) {
|
|
|
1171
1384
|
outputFile: summary.outputFile,
|
|
1172
1385
|
errors: summary.errors,
|
|
1173
1386
|
fatalErrorEncountered: isTerminalFailure(status),
|
|
1387
|
+
warnings,
|
|
1174
1388
|
startedAt,
|
|
1175
1389
|
endedAt
|
|
1176
1390
|
};
|
|
@@ -1193,6 +1407,62 @@ async function loadFullHistory(sfn, executionArn) {
|
|
|
1193
1407
|
}
|
|
1194
1408
|
return events;
|
|
1195
1409
|
}
|
|
1410
|
+
var FAILED_CHUNK_HISTORIES_READ = 5;
|
|
1411
|
+
var MAP_RUN_ACCESS_DENIED = "Chunk progress is hidden until the chunks finish: this caller lacks states:DescribeMapRun. Re-apply the policy from `hyperframes lambda policies user`.";
|
|
1412
|
+
function findChunkMapRun(events) {
|
|
1413
|
+
let run = null;
|
|
1414
|
+
for (const ev of events) {
|
|
1415
|
+
const arn = ev.mapRunStartedEventDetails?.mapRunArn;
|
|
1416
|
+
if (arn) run = { arn, exited: false, output: void 0 };
|
|
1417
|
+
const exit = ev.stateExitedEventDetails;
|
|
1418
|
+
if (run && ev.type === "MapStateExited" && isRenderChunksMapState(exit?.name)) {
|
|
1419
|
+
run = { ...run, exited: true, output: exit?.output };
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return run;
|
|
1423
|
+
}
|
|
1424
|
+
function addChunkResults(summary, mapOutput, memoryMb) {
|
|
1425
|
+
const chunks = parseJson(mapOutput)?.Chunks;
|
|
1426
|
+
for (const chunk of Array.isArray(chunks) ? chunks : []) {
|
|
1427
|
+
summary.lambdasInvoked++;
|
|
1428
|
+
summary.stateTransitions++;
|
|
1429
|
+
const billedDurationMs = inferBilledMs(chunk);
|
|
1430
|
+
summary.lambdaInvocations.push({
|
|
1431
|
+
billedDurationMs,
|
|
1432
|
+
memorySizeMb: memoryMb,
|
|
1433
|
+
estimated: billedDurationMs === 0
|
|
1434
|
+
});
|
|
1435
|
+
applyPayloadFrameCounts(chunk, "RenderChunk", (delta) => {
|
|
1436
|
+
summary.framesRendered += delta;
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
async function addRunningMapRun(sfn, mapRunArn, summary, memoryMb) {
|
|
1441
|
+
const res = await sfn.send(new DescribeMapRunCommand({ mapRunArn }));
|
|
1442
|
+
const { total = 0, pending = 0, succeeded = 0, failed = 0 } = res.itemCounts ?? {};
|
|
1443
|
+
summary.lambdasInvoked += total - pending;
|
|
1444
|
+
summary.stateTransitions += total - pending;
|
|
1445
|
+
if (summary.totalFrames !== null && total > 0) {
|
|
1446
|
+
summary.framesRendered += Math.floor(summary.totalFrames * succeeded / total);
|
|
1447
|
+
}
|
|
1448
|
+
if (failed > 0) summary.errors.push(...await failedChunkErrors(sfn, mapRunArn, memoryMb));
|
|
1449
|
+
}
|
|
1450
|
+
async function failedChunkErrors(sfn, mapRunArn, memoryMb) {
|
|
1451
|
+
const failed = await sfn.send(
|
|
1452
|
+
new ListExecutionsCommand({
|
|
1453
|
+
mapRunArn,
|
|
1454
|
+
statusFilter: "FAILED",
|
|
1455
|
+
maxResults: FAILED_CHUNK_HISTORIES_READ
|
|
1456
|
+
})
|
|
1457
|
+
);
|
|
1458
|
+
const errors = [];
|
|
1459
|
+
for (const child of failed.executions ?? []) {
|
|
1460
|
+
if (!child.executionArn) continue;
|
|
1461
|
+
const childHistory = await loadFullHistory(sfn, child.executionArn);
|
|
1462
|
+
errors.push(...summarizeHistory(childHistory, memoryMb).errors);
|
|
1463
|
+
}
|
|
1464
|
+
return errors;
|
|
1465
|
+
}
|
|
1196
1466
|
function summarizeHistory(events, memoryMb) {
|
|
1197
1467
|
let framesRendered = 0;
|
|
1198
1468
|
let totalFrames = null;
|
|
@@ -1351,6 +1621,9 @@ function applyPayloadFrameCounts(payload, currentLambdaState, bump) {
|
|
|
1351
1621
|
function isRenderChunkState(name) {
|
|
1352
1622
|
return name === "RenderChunk" || name === "RenderChunkV2";
|
|
1353
1623
|
}
|
|
1624
|
+
function isRenderChunksMapState(name) {
|
|
1625
|
+
return name === "RenderChunks" || name === "RenderChunksV2";
|
|
1626
|
+
}
|
|
1354
1627
|
function isAssembleState(name) {
|
|
1355
1628
|
return name === "Assemble" || name === "AssembleV2";
|
|
1356
1629
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
/** Bytes `[start, end)` of an S3 object. */
|
|
3
|
+
interface RangeSegment {
|
|
4
|
+
uri: string;
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
}
|
|
8
|
+
type Segment = {
|
|
9
|
+
bytes: Buffer;
|
|
10
|
+
} | RangeSegment;
|
|
11
|
+
export type Part = {
|
|
12
|
+
copy: RangeSegment;
|
|
13
|
+
} | {
|
|
14
|
+
upload: Segment[];
|
|
15
|
+
};
|
|
16
|
+
export declare function planParts(segments: readonly Segment[], min?: number): Part[];
|
|
17
|
+
/** 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>;
|
|
19
|
+
export interface StitchResult {
|
|
20
|
+
fileSize: number;
|
|
21
|
+
parts: number;
|
|
22
|
+
downloadedBytes: number;
|
|
23
|
+
}
|
|
24
|
+
/** One faststart mp4 at `destUri` from the stitched mp4 chunks at `chunkUris`, in order. */
|
|
25
|
+
export declare function stitchMp4InS3(s3: S3Client, chunkUris: readonly string[], destUri: string, minPartBytes?: number): Promise<StitchResult>;
|
|
26
|
+
export {};
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* `getRenderProgress` — read-only progress + cost snapshot for a single
|
|
3
3
|
* render started by {@link renderToLambda}.
|
|
4
4
|
*
|
|
5
|
-
* Pulls one `DescribeExecution` + one `GetExecutionHistory` per call.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Pulls one `DescribeExecution` + one `GetExecutionHistory` per call. While
|
|
6
|
+
* the chunk Distributed Map runs, one `DescribeMapRun` adds its item counts;
|
|
7
|
+
* once it exits, chunk frames and cost come from the Map's output. Failed
|
|
8
|
+
* chunks' histories are read only when the counts show a failure.
|
|
9
9
|
*
|
|
10
10
|
* Progress math:
|
|
11
11
|
* - 0 before Plan completes (no frame count is known yet)
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* `events.ts`, so the parser doesn't need to know anything beyond
|
|
20
20
|
* "JSON.parse this string and grab two fields."
|
|
21
21
|
*/
|
|
22
|
-
import { SFNClient } from "@aws-sdk/client-sfn";
|
|
22
|
+
import { SFNClient, type SFNClientConfig } from "@aws-sdk/client-sfn";
|
|
23
23
|
import { type RenderCost } from "./costAccounting.js";
|
|
24
24
|
/** Options for {@link getRenderProgress}. */
|
|
25
25
|
export interface GetRenderProgressOptions {
|
|
@@ -64,8 +64,12 @@ export interface RenderProgress {
|
|
|
64
64
|
errors: RenderError[];
|
|
65
65
|
/** `true` once the execution has terminated in a non-`SUCCEEDED` state. */
|
|
66
66
|
fatalErrorEncountered: boolean;
|
|
67
|
+
/** Problems reading progress that leave the render itself untouched, such as a missing IAM permission. */
|
|
68
|
+
warnings: string[];
|
|
67
69
|
startedAt: string;
|
|
68
70
|
endedAt: string | null;
|
|
69
71
|
}
|
|
72
|
+
/** Rides out `ThrottlingException` with the SDK's backoff instead of throwing after the default 3 tries. */
|
|
73
|
+
export declare function createProgressSfnClient(config?: SFNClientConfig): SFNClient;
|
|
70
74
|
/** Pull a current progress snapshot for one render. */
|
|
71
75
|
export declare function getRenderProgress(opts: GetRenderProgressOptions): Promise<RenderProgress>;
|
package/dist/sdk/index.js
CHANGED
|
@@ -227,7 +227,9 @@ async function renderToLambda(opts) {
|
|
|
227
227
|
// src/sdk/getRenderProgress.ts
|
|
228
228
|
import {
|
|
229
229
|
DescribeExecutionCommand,
|
|
230
|
+
DescribeMapRunCommand,
|
|
230
231
|
GetExecutionHistoryCommand,
|
|
232
|
+
ListExecutionsCommand,
|
|
231
233
|
SFNClient as SFNClient2
|
|
232
234
|
} from "@aws-sdk/client-sfn";
|
|
233
235
|
|
|
@@ -264,11 +266,14 @@ function formatUsd(usd) {
|
|
|
264
266
|
|
|
265
267
|
// src/sdk/getRenderProgress.ts
|
|
266
268
|
var DEFAULT_MEMORY_MB = 10240;
|
|
269
|
+
function createProgressSfnClient(config = {}) {
|
|
270
|
+
return new SFNClient2({ maxAttempts: 8, ...config });
|
|
271
|
+
}
|
|
267
272
|
async function getRenderProgress(opts) {
|
|
268
273
|
if (!opts.executionArn) {
|
|
269
274
|
throw new Error("[getRenderProgress] executionArn is required");
|
|
270
275
|
}
|
|
271
|
-
const sfn = opts.sfn ??
|
|
276
|
+
const sfn = opts.sfn ?? createProgressSfnClient({ region: opts.region });
|
|
272
277
|
const memoryMb = opts.defaultMemorySizeMb ?? DEFAULT_MEMORY_MB;
|
|
273
278
|
const describe = await sfn.send(
|
|
274
279
|
new DescribeExecutionCommand({ executionArn: opts.executionArn })
|
|
@@ -278,6 +283,15 @@ async function getRenderProgress(opts) {
|
|
|
278
283
|
const endedAt = describe.stopDate?.toISOString() ?? null;
|
|
279
284
|
const history = await loadFullHistory(sfn, opts.executionArn);
|
|
280
285
|
const summary = summarizeHistory(history, memoryMb);
|
|
286
|
+
const mapRun = findChunkMapRun(history);
|
|
287
|
+
const warnings = [];
|
|
288
|
+
if (mapRun?.exited) addChunkResults(summary, mapRun.output, memoryMb);
|
|
289
|
+
else if (mapRun) {
|
|
290
|
+
await addRunningMapRun(sfn, mapRun.arn, summary, memoryMb).catch((err) => {
|
|
291
|
+
if (err?.name !== "AccessDeniedException") throw err;
|
|
292
|
+
warnings.push(MAP_RUN_ACCESS_DENIED);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
281
295
|
const costs = computeRenderCost(summary.lambdaInvocations, summary.stateTransitions);
|
|
282
296
|
const overallProgress = computeOverallProgress({
|
|
283
297
|
status,
|
|
@@ -295,6 +309,7 @@ async function getRenderProgress(opts) {
|
|
|
295
309
|
outputFile: summary.outputFile,
|
|
296
310
|
errors: summary.errors,
|
|
297
311
|
fatalErrorEncountered: isTerminalFailure(status),
|
|
312
|
+
warnings,
|
|
298
313
|
startedAt,
|
|
299
314
|
endedAt
|
|
300
315
|
};
|
|
@@ -317,6 +332,62 @@ async function loadFullHistory(sfn, executionArn) {
|
|
|
317
332
|
}
|
|
318
333
|
return events;
|
|
319
334
|
}
|
|
335
|
+
var FAILED_CHUNK_HISTORIES_READ = 5;
|
|
336
|
+
var MAP_RUN_ACCESS_DENIED = "Chunk progress is hidden until the chunks finish: this caller lacks states:DescribeMapRun. Re-apply the policy from `hyperframes lambda policies user`.";
|
|
337
|
+
function findChunkMapRun(events) {
|
|
338
|
+
let run = null;
|
|
339
|
+
for (const ev of events) {
|
|
340
|
+
const arn = ev.mapRunStartedEventDetails?.mapRunArn;
|
|
341
|
+
if (arn) run = { arn, exited: false, output: void 0 };
|
|
342
|
+
const exit = ev.stateExitedEventDetails;
|
|
343
|
+
if (run && ev.type === "MapStateExited" && isRenderChunksMapState(exit?.name)) {
|
|
344
|
+
run = { ...run, exited: true, output: exit?.output };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return run;
|
|
348
|
+
}
|
|
349
|
+
function addChunkResults(summary, mapOutput, memoryMb) {
|
|
350
|
+
const chunks = parseJson(mapOutput)?.Chunks;
|
|
351
|
+
for (const chunk of Array.isArray(chunks) ? chunks : []) {
|
|
352
|
+
summary.lambdasInvoked++;
|
|
353
|
+
summary.stateTransitions++;
|
|
354
|
+
const billedDurationMs = inferBilledMs(chunk);
|
|
355
|
+
summary.lambdaInvocations.push({
|
|
356
|
+
billedDurationMs,
|
|
357
|
+
memorySizeMb: memoryMb,
|
|
358
|
+
estimated: billedDurationMs === 0
|
|
359
|
+
});
|
|
360
|
+
applyPayloadFrameCounts(chunk, "RenderChunk", (delta) => {
|
|
361
|
+
summary.framesRendered += delta;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function addRunningMapRun(sfn, mapRunArn, summary, memoryMb) {
|
|
366
|
+
const res = await sfn.send(new DescribeMapRunCommand({ mapRunArn }));
|
|
367
|
+
const { total = 0, pending = 0, succeeded = 0, failed = 0 } = res.itemCounts ?? {};
|
|
368
|
+
summary.lambdasInvoked += total - pending;
|
|
369
|
+
summary.stateTransitions += total - pending;
|
|
370
|
+
if (summary.totalFrames !== null && total > 0) {
|
|
371
|
+
summary.framesRendered += Math.floor(summary.totalFrames * succeeded / total);
|
|
372
|
+
}
|
|
373
|
+
if (failed > 0) summary.errors.push(...await failedChunkErrors(sfn, mapRunArn, memoryMb));
|
|
374
|
+
}
|
|
375
|
+
async function failedChunkErrors(sfn, mapRunArn, memoryMb) {
|
|
376
|
+
const failed = await sfn.send(
|
|
377
|
+
new ListExecutionsCommand({
|
|
378
|
+
mapRunArn,
|
|
379
|
+
statusFilter: "FAILED",
|
|
380
|
+
maxResults: FAILED_CHUNK_HISTORIES_READ
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
const errors = [];
|
|
384
|
+
for (const child of failed.executions ?? []) {
|
|
385
|
+
if (!child.executionArn) continue;
|
|
386
|
+
const childHistory = await loadFullHistory(sfn, child.executionArn);
|
|
387
|
+
errors.push(...summarizeHistory(childHistory, memoryMb).errors);
|
|
388
|
+
}
|
|
389
|
+
return errors;
|
|
390
|
+
}
|
|
320
391
|
function summarizeHistory(events, memoryMb) {
|
|
321
392
|
let framesRendered = 0;
|
|
322
393
|
let totalFrames = null;
|
|
@@ -475,6 +546,9 @@ function applyPayloadFrameCounts(payload, currentLambdaState, bump) {
|
|
|
475
546
|
function isRenderChunkState(name) {
|
|
476
547
|
return name === "RenderChunk" || name === "RenderChunkV2";
|
|
477
548
|
}
|
|
549
|
+
function isRenderChunksMapState(name) {
|
|
550
|
+
return name === "RenderChunks" || name === "RenderChunksV2";
|
|
551
|
+
}
|
|
478
552
|
function isAssembleState(name) {
|
|
479
553
|
return name === "Assemble" || name === "AssembleV2";
|
|
480
554
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/aws-lambda",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.174",
|
|
4
4
|
"description": "AWS Lambda adapter for HyperFrames distributed rendering — handler, client-side SDK, and CDK construct.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"registry": "https://registry.npmjs.org/"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"@aws-sdk/client-lambda": "^3.1146.0",
|
|
41
42
|
"@aws-sdk/client-s3": "^3.1146.0",
|
|
42
43
|
"@aws-sdk/client-sfn": "^3.1146.0",
|
|
43
44
|
"@sparticuz/chromium": "148.0.0",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"ffprobe-static": "^3.1.0",
|
|
46
47
|
"puppeteer-core": "^25.8.0",
|
|
47
48
|
"tar": "^7.4.3",
|
|
48
|
-
"@hyperframes/producer": "^0.8.
|
|
49
|
+
"@hyperframes/producer": "^0.8.174"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@types/aws-lambda": "^8.10.146",
|
package/scripts/build-zip.ts
CHANGED
|
@@ -228,6 +228,7 @@ async function bundleHandler(stagingDir: string): Promise<void> {
|
|
|
228
228
|
"puppeteer",
|
|
229
229
|
// AWS SDK v3 is pre-installed in the Lambda Node 22 runtime; mark
|
|
230
230
|
// external so we don't double-bundle 3+ MiB of SDK.
|
|
231
|
+
"@aws-sdk/client-lambda",
|
|
231
232
|
"@aws-sdk/client-s3",
|
|
232
233
|
],
|
|
233
234
|
plugins: [workspaceAliasPlugin],
|