@hyperframes/aws-lambda 0.8.209 → 0.8.211
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/_chunks/{chunk-OTRGQBLD.js → chunk-QU5TSGIK.js} +134 -26
- package/dist/cdk/index.js +15 -2
- package/dist/events.d.ts +21 -2
- package/dist/handler.d.ts +6 -0
- package/dist/handler.js +3 -1
- package/dist/index.js +1 -1
- package/dist/s3Stitch.d.ts +17 -2
- package/package.json +2 -2
- package/prebuilt/handler-zip.json +4 -4
- package/prebuilt/template.yaml +12 -0
- package/prebuilt/video-function.yaml +5 -0
- package/prebuilt/video-image.json +4 -4
|
@@ -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
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
574
|
+
function reservedConcurrency() {
|
|
512
575
|
const reserved = Number(process.env.HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY);
|
|
513
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
package/dist/s3Stitch.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.8.211",
|
|
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.
|
|
55
|
+
"@hyperframes/producer": "^0.8.211"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/aws-lambda": "^8.10.146",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.8.
|
|
3
|
-
"url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.
|
|
4
|
-
"sha256": "
|
|
5
|
-
"bytes":
|
|
2
|
+
"version": "0.8.211",
|
|
3
|
+
"url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.211/hyperframes-lambda-handler.zip",
|
|
4
|
+
"sha256": "35fc9f082e8a753c517a530b27db89423a4c6c36a0ac0421e3c56d882a4f8c9b",
|
|
5
|
+
"bytes": 129299191
|
|
6
6
|
}
|
package/prebuilt/template.yaml
CHANGED
|
@@ -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.
|
|
3
|
-
"url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.
|
|
4
|
-
"sha256": "
|
|
5
|
-
"bytes":
|
|
2
|
+
"version": "0.8.211",
|
|
3
|
+
"url": "https://github.com/heygen-com/hyperframes/releases/download/v0.8.211/hyperframes-lambda-video-arm64.oci.tar",
|
|
4
|
+
"sha256": "c8b4eb268c992dda44b20a9cec45c53731e43c06a1eadbac1f42b6a07ce42c96",
|
|
5
|
+
"bytes": 244679168
|
|
6
6
|
}
|