@remotion/lambda 4.0.0-fastlambda.8 → 4.0.0-lambda.3
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/admin/make-layer-public.js +4 -4
- package/dist/api/clean-items.js +2 -5
- package/dist/api/create-function.d.ts +2 -1
- package/dist/api/create-function.js +3 -2
- package/dist/api/deploy-function.d.ts +1 -0
- package/dist/api/deploy-function.js +3 -0
- package/dist/api/get-aws-client.js +5 -1
- package/dist/api/upload-dir.js +4 -3
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/commands/functions/deploy.js +6 -2
- package/dist/cli/commands/render/render.js +8 -0
- package/dist/cli/index.js +10 -0
- package/dist/defaults.js +5 -1
- package/dist/functions/chunk-optimization/get-frame-ranges-from-profile.d.ts +1 -4
- package/dist/functions/chunk-optimization/optimize-profile.d.ts +2 -8
- package/dist/functions/chunk-optimization/plan-frame-ranges.d.ts +1 -4
- package/dist/functions/chunk-optimization/simulate-frame-ranges.d.ts +1 -4
- package/dist/functions/helpers/concat-videos.d.ts +2 -1
- package/dist/functions/helpers/concat-videos.js +7 -2
- package/dist/functions/helpers/create-post-render-data.d.ts +1 -1
- package/dist/functions/helpers/create-post-render-data.js +5 -5
- package/dist/functions/helpers/enhanced-error-info.d.ts +24 -0
- package/dist/functions/helpers/enhanced-error-info.js +2 -0
- package/dist/functions/helpers/get-browser-instance.js +3 -1
- package/dist/functions/helpers/get-files-to-delete.js +2 -2
- package/dist/functions/helpers/get-progress.js +14 -11
- package/dist/functions/helpers/io.js +5 -0
- package/dist/functions/helpers/validate-composition.d.ts +8 -3
- package/dist/functions/helpers/validate-composition.js +6 -1
- package/dist/functions/launch.js +6 -0
- package/dist/functions/renderer.js +3 -5
- package/dist/functions/still.js +6 -0
- package/dist/shared/constants.d.ts +4 -1
- package/dist/shared/constants.js +1 -1
- package/dist/shared/get-most-expensive-chunks.d.ts +8 -0
- package/dist/shared/get-most-expensive-chunks.js +25 -0
- package/dist/shared/hosted-layers.js +80 -80
- package/dist/shared/make-s3-key.d.ts +1 -0
- package/dist/shared/make-s3-key.js +11 -0
- package/dist/shared/p-limit.d.ts +1 -0
- package/dist/shared/p-limit.js +57 -0
- package/dist/shared/random-hash.d.ts +2 -2
- package/dist/shared/random-hash.js +1 -1
- package/dist/shared/stream-to-string.d.ts +1 -0
- package/dist/shared/validate-custom-role-arn.d.ts +1 -0
- package/dist/shared/validate-custom-role-arn.js +11 -0
- package/package.json +10 -12
- package/remotionlambda.zip +0 -0
|
@@ -35,7 +35,7 @@ const layerInfo = {
|
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
37
|
const makeLayerPublic = async () => {
|
|
38
|
-
const layers = ['
|
|
38
|
+
const layers = ['fonts', 'ffmpeg', 'chromium'];
|
|
39
39
|
for (const architecture of archictures) {
|
|
40
40
|
for (const region of (0, __1.getRegions)()) {
|
|
41
41
|
for (const layer of layers) {
|
|
@@ -43,14 +43,14 @@ const makeLayerPublic = async () => {
|
|
|
43
43
|
const { Version, LayerArn } = await (0, aws_clients_1.getLambdaClient)(region).send(new client_lambda_1.PublishLayerVersionCommand({
|
|
44
44
|
Content: {
|
|
45
45
|
S3Bucket: 'remotionlambda-binaries-' + region,
|
|
46
|
-
S3Key: `remotion-layer-${layer}-
|
|
46
|
+
S3Key: `remotion-layer-${layer}-v6-${architecture}.zip`,
|
|
47
47
|
},
|
|
48
48
|
LayerName: layerName,
|
|
49
49
|
LicenseInfo: layer === 'chromium'
|
|
50
|
-
? '
|
|
50
|
+
? 'Chromium 101, compiled from source. Read Chromium License: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/LICENSE'
|
|
51
51
|
: layer === 'ffmpeg'
|
|
52
52
|
? 'Compiled from FFMPEG source. Read FFMPEG license: https://ffmpeg.org/legal.html'
|
|
53
|
-
: 'Contains
|
|
53
|
+
: 'Contains Noto Sans font. Read Noto Sans License: https://fonts.google.com/noto/specimen/Noto+Sans/about',
|
|
54
54
|
CompatibleRuntimes: runtimes,
|
|
55
55
|
Description: constants_1.CURRENT_VERSION,
|
|
56
56
|
}));
|
package/dist/api/clean-items.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.cleanItems = void 0;
|
|
7
4
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
8
|
-
const p_limit_1 = __importDefault(require("p-limit"));
|
|
9
5
|
const aws_clients_1 = require("../shared/aws-clients");
|
|
10
|
-
const
|
|
6
|
+
const p_limit_1 = require("../shared/p-limit");
|
|
7
|
+
const limit = (0, p_limit_1.pLimit)(10);
|
|
11
8
|
const cleanItems = async ({ bucket, onAfterItemDeleted, onBeforeItemDeleted, region, list, }) => {
|
|
12
9
|
return Promise.all(list.map((object) => limit(async () => {
|
|
13
10
|
onBeforeItemDeleted({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AwsRegion } from '../pricing/aws-regions';
|
|
2
2
|
import { LambdaArchitecture } from '../shared/validate-architecture';
|
|
3
|
-
export declare const createFunction: ({ createCloudWatchLogGroup, region, zipFile, functionName, accountId, memorySizeInMb, timeoutInSeconds, alreadyCreated, retentionInDays, architecture, ephemerealStorageInMb, }: {
|
|
3
|
+
export declare const createFunction: ({ createCloudWatchLogGroup, region, zipFile, functionName, accountId, memorySizeInMb, timeoutInSeconds, alreadyCreated, retentionInDays, architecture, ephemerealStorageInMb, customRoleArn, }: {
|
|
4
4
|
createCloudWatchLogGroup: boolean;
|
|
5
5
|
region: AwsRegion;
|
|
6
6
|
zipFile: string;
|
|
@@ -12,6 +12,7 @@ export declare const createFunction: ({ createCloudWatchLogGroup, region, zipFil
|
|
|
12
12
|
retentionInDays: number;
|
|
13
13
|
ephemerealStorageInMb: number;
|
|
14
14
|
architecture: LambdaArchitecture;
|
|
15
|
+
customRoleArn: string;
|
|
15
16
|
}) => Promise<{
|
|
16
17
|
FunctionName: string;
|
|
17
18
|
}>;
|
|
@@ -8,7 +8,7 @@ const defaults_1 = require("../defaults");
|
|
|
8
8
|
const aws_clients_1 = require("../shared/aws-clients");
|
|
9
9
|
const hosted_layers_1 = require("../shared/hosted-layers");
|
|
10
10
|
const suggested_policy_1 = require("./iam-validation/suggested-policy");
|
|
11
|
-
const createFunction = async ({ createCloudWatchLogGroup, region, zipFile, functionName, accountId, memorySizeInMb, timeoutInSeconds, alreadyCreated, retentionInDays, architecture, ephemerealStorageInMb, }) => {
|
|
11
|
+
const createFunction = async ({ createCloudWatchLogGroup, region, zipFile, functionName, accountId, memorySizeInMb, timeoutInSeconds, alreadyCreated, retentionInDays, architecture, ephemerealStorageInMb, customRoleArn, }) => {
|
|
12
12
|
if (createCloudWatchLogGroup) {
|
|
13
13
|
try {
|
|
14
14
|
await (0, aws_clients_1.getCloudWatchLogsClient)(region).send(new client_cloudwatch_logs_1.CreateLogGroupCommand({
|
|
@@ -29,13 +29,14 @@ const createFunction = async ({ createCloudWatchLogGroup, region, zipFile, funct
|
|
|
29
29
|
if (alreadyCreated) {
|
|
30
30
|
return { FunctionName: functionName };
|
|
31
31
|
}
|
|
32
|
+
const defaultRoleName = `arn:aws:iam::${accountId}:role/${suggested_policy_1.ROLE_NAME}`;
|
|
32
33
|
const { FunctionName } = await (0, aws_clients_1.getLambdaClient)(region).send(new client_lambda_1.CreateFunctionCommand({
|
|
33
34
|
Code: {
|
|
34
35
|
ZipFile: (0, fs_1.readFileSync)(zipFile),
|
|
35
36
|
},
|
|
36
37
|
FunctionName: functionName,
|
|
37
38
|
Handler: 'index.handler',
|
|
38
|
-
Role:
|
|
39
|
+
Role: customRoleArn !== null && customRoleArn !== void 0 ? customRoleArn : defaultRoleName,
|
|
39
40
|
Runtime: 'nodejs14.x',
|
|
40
41
|
Description: 'Renders a Remotion video.',
|
|
41
42
|
MemorySize: memorySizeInMb,
|
|
@@ -7,6 +7,7 @@ const function_zip_path_1 = require("../shared/function-zip-path");
|
|
|
7
7
|
const get_account_id_1 = require("../shared/get-account-id");
|
|
8
8
|
const validate_architecture_1 = require("../shared/validate-architecture");
|
|
9
9
|
const validate_aws_region_1 = require("../shared/validate-aws-region");
|
|
10
|
+
const validate_custom_role_arn_1 = require("../shared/validate-custom-role-arn");
|
|
10
11
|
const validate_disk_size_in_mb_1 = require("../shared/validate-disk-size-in-mb");
|
|
11
12
|
const validate_memory_size_1 = require("../shared/validate-memory-size");
|
|
12
13
|
const validate_retention_period_1 = require("../shared/validate-retention-period");
|
|
@@ -33,6 +34,7 @@ const deployFunction = async (options) => {
|
|
|
33
34
|
(0, validate_retention_period_1.validateCloudWatchRetentionPeriod)(options.cloudWatchLogRetentionPeriodInDays);
|
|
34
35
|
(0, validate_architecture_1.validateArchitecture)(options.architecture);
|
|
35
36
|
(0, validate_disk_size_in_mb_1.validateDiskSizeInMb)(diskSizeInMb);
|
|
37
|
+
(0, validate_custom_role_arn_1.validateCustomRoleArn)(options.customRoleArn);
|
|
36
38
|
const fnNameRender = [
|
|
37
39
|
`${constants_1.RENDER_FN_PREFIX}${constants_1.CURRENT_VERSION}`,
|
|
38
40
|
`mem${options.memorySizeInMb}mb`,
|
|
@@ -60,6 +62,7 @@ const deployFunction = async (options) => {
|
|
|
60
62
|
alreadyCreated: Boolean(alreadyDeployed),
|
|
61
63
|
architecture: options.architecture,
|
|
62
64
|
ephemerealStorageInMb: diskSizeInMb,
|
|
65
|
+
customRoleArn: options.customRoleArn,
|
|
63
66
|
});
|
|
64
67
|
if (!created.FunctionName) {
|
|
65
68
|
throw new Error('Lambda was created but has no name');
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
package/dist/api/upload-dir.js
CHANGED
|
@@ -6,10 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.uploadDir = exports.getDirFiles = void 0;
|
|
7
7
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
8
8
|
const lib_storage_1 = require("@aws-sdk/lib-storage");
|
|
9
|
-
const bundler_1 = require("@remotion/bundler");
|
|
10
9
|
const fs_1 = require("fs");
|
|
10
|
+
const mime_types_1 = __importDefault(require("mime-types"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const aws_clients_1 = require("../shared/aws-clients");
|
|
13
|
+
const make_s3_key_1 = require("../shared/make-s3-key");
|
|
13
14
|
const getDirFiles = (entry) => {
|
|
14
15
|
throw new TypeError('should only be executed in test ' + JSON.stringify(entry));
|
|
15
16
|
};
|
|
@@ -38,9 +39,9 @@ const uploadDir = async ({ bucket, region, dir, onProgress, folder, privacy, })
|
|
|
38
39
|
}
|
|
39
40
|
const client = (0, aws_clients_1.getS3Client)(region);
|
|
40
41
|
const uploads = files.map(async (filePath) => {
|
|
41
|
-
const Key =
|
|
42
|
+
const Key = (0, make_s3_key_1.makeS3Key)(folder, dir, filePath.name);
|
|
42
43
|
const Body = (0, fs_1.createReadStream)(filePath.name);
|
|
43
|
-
const ContentType =
|
|
44
|
+
const ContentType = mime_types_1.default.lookup(Key) || 'application/octet-stream';
|
|
44
45
|
const ACL = privacy === 'private' ? 'private' : 'public-read';
|
|
45
46
|
if (filePath.size > 5 * 1024 * 1024) {
|
|
46
47
|
const paralellUploads3 = new lib_storage_1.Upload({
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ declare type LambdaCommandLineOptions = {
|
|
|
21
21
|
['frames-per-lambda']: number;
|
|
22
22
|
['out-name']: string | undefined;
|
|
23
23
|
['architecture']: LambdaArchitecture;
|
|
24
|
+
['custom-role-arn']: string | undefined;
|
|
24
25
|
privacy: Privacy;
|
|
25
26
|
};
|
|
26
27
|
export declare const parsedLambdaCli: LambdaCommandLineOptions & minimist.ParsedArgs;
|
|
@@ -6,6 +6,7 @@ const log_1 = require("@remotion/cli/dist/log");
|
|
|
6
6
|
const deploy_function_1 = require("../../../api/deploy-function");
|
|
7
7
|
const constants_1 = require("../../../shared/constants");
|
|
8
8
|
const validate_architecture_1 = require("../../../shared/validate-architecture");
|
|
9
|
+
const validate_custom_role_arn_1 = require("../../../shared/validate-custom-role-arn");
|
|
9
10
|
const validate_disk_size_in_mb_1 = require("../../../shared/validate-disk-size-in-mb");
|
|
10
11
|
const validate_memory_size_1 = require("../../../shared/validate-memory-size");
|
|
11
12
|
const validate_timeout_1 = require("../../../shared/validate-timeout");
|
|
@@ -13,18 +14,20 @@ const args_1 = require("../../args");
|
|
|
13
14
|
const get_aws_region_1 = require("../../get-aws-region");
|
|
14
15
|
exports.FUNCTIONS_DEPLOY_SUBCOMMAND = 'deploy';
|
|
15
16
|
const functionsDeploySubcommand = async () => {
|
|
16
|
-
var _a, _b, _c, _d, _e;
|
|
17
|
+
var _a, _b, _c, _d, _e, _f;
|
|
17
18
|
const region = (0, get_aws_region_1.getAwsRegion)();
|
|
18
19
|
const timeoutInSeconds = (_a = args_1.parsedLambdaCli.timeout) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_TIMEOUT;
|
|
19
20
|
const memorySizeInMb = (_b = args_1.parsedLambdaCli.memory) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_MEMORY_SIZE;
|
|
20
21
|
const diskSizeInMb = (_c = args_1.parsedLambdaCli.disk) !== null && _c !== void 0 ? _c : constants_1.DEFAULT_EPHEMERAL_STORAGE_IN_MB;
|
|
21
22
|
const architecture = (_d = args_1.parsedLambdaCli.architecture) !== null && _d !== void 0 ? _d : constants_1.DEFAULT_ARCHITECTURE;
|
|
23
|
+
const customRoleArn = (_e = args_1.parsedLambdaCli['custom-role-arn']) !== null && _e !== void 0 ? _e : undefined;
|
|
22
24
|
const createCloudWatchLogGroup = !args_1.parsedLambdaCli['disable-cloudwatch'];
|
|
23
|
-
const cloudWatchLogRetentionPeriodInDays = (
|
|
25
|
+
const cloudWatchLogRetentionPeriodInDays = (_f = args_1.parsedLambdaCli['retention-period']) !== null && _f !== void 0 ? _f : constants_1.DEFAULT_CLOUDWATCH_RETENTION_PERIOD;
|
|
24
26
|
(0, validate_memory_size_1.validateMemorySize)(memorySizeInMb);
|
|
25
27
|
(0, validate_timeout_1.validateTimeout)(timeoutInSeconds);
|
|
26
28
|
(0, validate_architecture_1.validateArchitecture)(architecture);
|
|
27
29
|
(0, validate_disk_size_in_mb_1.validateDiskSizeInMb)(diskSizeInMb);
|
|
30
|
+
(0, validate_custom_role_arn_1.validateCustomRoleArn)(customRoleArn);
|
|
28
31
|
if (!cli_1.CliInternals.quietFlagProvided()) {
|
|
29
32
|
log_1.Log.info(cli_1.CliInternals.chalk.gray(`
|
|
30
33
|
Region = ${region}
|
|
@@ -47,6 +50,7 @@ CloudWatch Retention Period = ${cloudWatchLogRetentionPeriodInDays} days
|
|
|
47
50
|
cloudWatchLogRetentionPeriodInDays,
|
|
48
51
|
architecture,
|
|
49
52
|
diskSizeInMb,
|
|
53
|
+
customRoleArn,
|
|
50
54
|
});
|
|
51
55
|
if (cli_1.CliInternals.quietFlagProvided()) {
|
|
52
56
|
log_1.Log.info(functionName);
|
|
@@ -169,6 +169,14 @@ const renderCommand = async (args) => {
|
|
|
169
169
|
]
|
|
170
170
|
.filter(Boolean)
|
|
171
171
|
.join(', '));
|
|
172
|
+
if (newStatus.mostExpensiveFrameRanges) {
|
|
173
|
+
log_1.Log.verbose('Most expensive frame ranges:');
|
|
174
|
+
log_1.Log.verbose(newStatus.mostExpensiveFrameRanges
|
|
175
|
+
.map((f) => {
|
|
176
|
+
return `${f.frameRange[0]}-${f.frameRange[1]} (${f.timeInMilliseconds}ms)`;
|
|
177
|
+
})
|
|
178
|
+
.join(', '));
|
|
179
|
+
}
|
|
172
180
|
(0, quit_1.quit)(0);
|
|
173
181
|
}
|
|
174
182
|
if (newStatus.fatalErrorEncountered) {
|
package/dist/cli/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.cli = exports.executeCommand = void 0;
|
|
4
4
|
const cli_1 = require("@remotion/cli");
|
|
5
5
|
const suggested_policy_1 = require("../api/iam-validation/suggested-policy");
|
|
6
|
+
const defaults_1 = require("../defaults");
|
|
6
7
|
const check_credentials_1 = require("../shared/check-credentials");
|
|
7
8
|
const docs_url_1 = require("../shared/docs-url");
|
|
8
9
|
const args_1 = require("./args");
|
|
@@ -99,6 +100,15 @@ const executeCommand = async (args) => {
|
|
|
99
100
|
catch (err) {
|
|
100
101
|
const error = err;
|
|
101
102
|
if (error.message.includes('The role defined for the function cannot be assumed by Lambda')) {
|
|
103
|
+
if (args_1.parsedLambdaCli['custom-role-arn']) {
|
|
104
|
+
log_1.Log.error(`
|
|
105
|
+
The role "${args_1.parsedLambdaCli['custom-role-arn']}" does not exist or has the wrong policy assigned to it. Do either:
|
|
106
|
+
- Remove the "--custom-role-arn" parameter and set up Remotion Lambda according to the setup guide
|
|
107
|
+
- Make sure the role has the same policy assigned as the one returned by "npx ${defaults_1.BINARY_NAME} ${policies_1.POLICIES_COMMAND} ${role_1.ROLE_SUBCOMMAND}"
|
|
108
|
+
|
|
109
|
+
Revisit ${docs_url_1.DOCS_URL}/docs/lambda/setup and make sure you set up the role and role policy correctly. Also see the troubleshooting page: ${docs_url_1.DOCS_URL}/docs/lambda/troubleshooting/permissions. The original error message is:
|
|
110
|
+
`.trim());
|
|
111
|
+
}
|
|
102
112
|
log_1.Log.error(`
|
|
103
113
|
The role "${suggested_policy_1.ROLE_NAME}" does not exist in your AWS account or has the wrong policy assigned to it. Common reasons:
|
|
104
114
|
- The name of the role is not "${suggested_policy_1.ROLE_NAME}"
|
package/dist/defaults.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
import { TimingProfile } from './types';
|
|
2
|
-
export declare const getFrameRangesFromProfile: (profile: TimingProfile) => [
|
|
3
|
-
number,
|
|
4
|
-
number
|
|
5
|
-
][];
|
|
2
|
+
export declare const getFrameRangesFromProfile: (profile: TimingProfile) => [number, number][];
|
|
6
3
|
export declare const sortProfileByFrameRanges: (profile: TimingProfile) => import("./types").ChunkTimingData[];
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { TimingProfile } from './types';
|
|
2
2
|
export declare const assignFrameToOther: ({ frameRanges, fromChunk, toChunk, framesToShift, }: {
|
|
3
|
-
frameRanges: [
|
|
4
|
-
number,
|
|
5
|
-
number
|
|
6
|
-
][];
|
|
3
|
+
frameRanges: [number, number][];
|
|
7
4
|
fromChunk: number;
|
|
8
5
|
toChunk: number;
|
|
9
6
|
framesToShift: number;
|
|
10
|
-
}) => [
|
|
11
|
-
number,
|
|
12
|
-
number
|
|
13
|
-
][];
|
|
7
|
+
}) => [number, number][];
|
|
14
8
|
export declare const optimizeProfile: (_profile: TimingProfile) => TimingProfile;
|
|
15
9
|
export declare const optimizeProfileRecursively: (profile: TimingProfile, amount: number) => TimingProfile;
|
|
@@ -6,9 +6,6 @@ export declare const planFrameRanges: ({ chunkCount, framesPerLambda, optimizati
|
|
|
6
6
|
shouldUseOptimization: boolean;
|
|
7
7
|
frameRange: [number, number];
|
|
8
8
|
}) => {
|
|
9
|
-
chunks: [
|
|
10
|
-
number,
|
|
11
|
-
number
|
|
12
|
-
][];
|
|
9
|
+
chunks: [number, number][];
|
|
13
10
|
didUseOptimization: boolean;
|
|
14
11
|
};
|
|
@@ -3,8 +3,5 @@ export declare const getTimingForFrame: (profile: TimingProfile, frame: number)
|
|
|
3
3
|
export declare const getSimulatedTimingForFrameRange: (profile: TimingProfile, frameRange: [number, number]) => ChunkTimingData['timings'];
|
|
4
4
|
export declare const simulateFrameRanges: ({ profile, newFrameRanges, }: {
|
|
5
5
|
profile: TimingProfile;
|
|
6
|
-
newFrameRanges: [
|
|
7
|
-
number,
|
|
8
|
-
number
|
|
9
|
-
][];
|
|
6
|
+
newFrameRanges: [number, number][];
|
|
10
7
|
}) => TimingProfile;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Codec } from 'remotion';
|
|
2
2
|
import { AwsRegion } from '../../pricing/aws-regions';
|
|
3
|
-
export declare const concatVideosS3: ({ bucket, expectedFiles, onProgress, numberOfFrames, renderId, region, codec, expectedBucketOwner, }: {
|
|
3
|
+
export declare const concatVideosS3: ({ bucket, expectedFiles, onProgress, numberOfFrames, renderId, region, codec, expectedBucketOwner, fps, }: {
|
|
4
4
|
bucket: string;
|
|
5
5
|
expectedFiles: number;
|
|
6
6
|
onProgress: (frames: number, encodingStart: number) => void;
|
|
@@ -9,6 +9,7 @@ export declare const concatVideosS3: ({ bucket, expectedFiles, onProgress, numbe
|
|
|
9
9
|
region: AwsRegion;
|
|
10
10
|
codec: Codec;
|
|
11
11
|
expectedBucketOwner: string;
|
|
12
|
+
fps: number;
|
|
12
13
|
}) => Promise<{
|
|
13
14
|
outfile: string;
|
|
14
15
|
cleanupChunksProm: Promise<void>;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
|
@@ -108,7 +112,7 @@ const getAllFilesS3 = ({ bucket, expectedFiles, outdir, renderId, region, expect
|
|
|
108
112
|
loop();
|
|
109
113
|
});
|
|
110
114
|
};
|
|
111
|
-
const concatVideosS3 = async ({ bucket, expectedFiles, onProgress, numberOfFrames, renderId, region, codec, expectedBucketOwner, }) => {
|
|
115
|
+
const concatVideosS3 = async ({ bucket, expectedFiles, onProgress, numberOfFrames, renderId, region, codec, expectedBucketOwner, fps, }) => {
|
|
112
116
|
var _a;
|
|
113
117
|
const outdir = (0, path_1.join)(renderer_1.RenderInternals.tmpDir(constants_1.CONCAT_FOLDER_TOKEN), 'bucket');
|
|
114
118
|
if ((0, fs_1.existsSync)(outdir)) {
|
|
@@ -137,6 +141,7 @@ const concatVideosS3 = async ({ bucket, expectedFiles, onProgress, numberOfFrame
|
|
|
137
141
|
onProgress: (p) => onProgress(p, encodingStart),
|
|
138
142
|
numberOfFrames,
|
|
139
143
|
codec: codecForCombining,
|
|
144
|
+
fps,
|
|
140
145
|
});
|
|
141
146
|
combine.end();
|
|
142
147
|
const cleanupChunksProm = ((_a = fs_1.default.promises.rm) !== null && _a !== void 0 ? _a : fs_1.default.promises.rmdir)(outdir, {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createPostRenderData = void 0;
|
|
4
4
|
const estimate_price_1 = require("../../api/estimate-price");
|
|
5
5
|
const constants_1 = require("../../shared/constants");
|
|
6
|
+
const get_most_expensive_chunks_1 = require("../../shared/get-most-expensive-chunks");
|
|
6
7
|
const parse_lambda_timings_key_1 = require("../../shared/parse-lambda-timings-key");
|
|
7
8
|
const calculate_chunk_times_1 = require("./calculate-chunk-times");
|
|
8
9
|
const get_current_architecture_1 = require("./get-current-architecture");
|
|
@@ -10,13 +11,12 @@ const get_files_to_delete_1 = require("./get-files-to-delete");
|
|
|
10
11
|
const get_lambdas_invoked_stats_1 = require("./get-lambdas-invoked-stats");
|
|
11
12
|
const get_retry_stats_1 = require("./get-retry-stats");
|
|
12
13
|
const get_time_to_finish_1 = require("./get-time-to-finish");
|
|
13
|
-
const
|
|
14
|
-
const createPostRenderData = async ({ renderId, region, memorySizeInMb, renderMetadata, contents, timeToEncode, errorExplanations, timeToDelete, outputFile, }) => {
|
|
14
|
+
const createPostRenderData = ({ renderId, region, memorySizeInMb, renderMetadata, contents, timeToEncode, errorExplanations, timeToDelete, outputFile, }) => {
|
|
15
15
|
var _a;
|
|
16
16
|
const initializedKeys = contents.filter((c) => { var _a; return (_a = c.Key) === null || _a === void 0 ? void 0 : _a.startsWith((0, constants_1.lambdaTimingsPrefix)(renderId)); });
|
|
17
17
|
const parsedTimings = initializedKeys.map(({ Key }) => (0, parse_lambda_timings_key_1.parseLambdaTimingsKey)(Key));
|
|
18
18
|
const times = parsedTimings
|
|
19
|
-
.map((p) => p.rendered - p.start + OVERHEAD_TIME_PER_LAMBDA)
|
|
19
|
+
.map((p) => p.rendered - p.start + get_most_expensive_chunks_1.OVERHEAD_TIME_PER_LAMBDA)
|
|
20
20
|
.reduce((a, b) => a + b);
|
|
21
21
|
const cost = (0, estimate_price_1.estimatePrice)({
|
|
22
22
|
durationInMiliseconds: times,
|
|
@@ -47,7 +47,7 @@ const createPostRenderData = async ({ renderId, region, memorySizeInMb, renderMe
|
|
|
47
47
|
if (timeToInvokeLambdas === null) {
|
|
48
48
|
throw new Error('should have timing for all lambdas');
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
return {
|
|
51
51
|
cost: {
|
|
52
52
|
currency: 'USD',
|
|
53
53
|
disclaimer: 'Estimated cost for lambda invocations only. Does not include cost for S3 storage and data transfer.',
|
|
@@ -78,7 +78,7 @@ const createPostRenderData = async ({ renderId, region, memorySizeInMb, renderMe
|
|
|
78
78
|
}),
|
|
79
79
|
timeToInvokeLambdas,
|
|
80
80
|
retriesInfo,
|
|
81
|
+
mostExpensiveFrameRanges: (0, get_most_expensive_chunks_1.getMostExpensiveChunks)(parsedTimings, renderMetadata.framesPerLambda),
|
|
81
82
|
};
|
|
82
|
-
return data;
|
|
83
83
|
};
|
|
84
84
|
exports.createPostRenderData = createPostRenderData;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare type FileNameAndSize = {
|
|
2
|
+
filename: string;
|
|
3
|
+
size: number;
|
|
4
|
+
};
|
|
5
|
+
export declare type LambdaErrorInfo = {
|
|
6
|
+
type: 'renderer' | 'browser' | 'stitcher';
|
|
7
|
+
message: string;
|
|
8
|
+
name: string;
|
|
9
|
+
stack: string;
|
|
10
|
+
frame: number | null;
|
|
11
|
+
chunk: number | null;
|
|
12
|
+
isFatal: boolean;
|
|
13
|
+
attempt: number;
|
|
14
|
+
willRetry: boolean;
|
|
15
|
+
totalAttempts: number;
|
|
16
|
+
tmpDir: {
|
|
17
|
+
files: FileNameAndSize[];
|
|
18
|
+
total: number;
|
|
19
|
+
} | null;
|
|
20
|
+
};
|
|
21
|
+
export declare type EnhancedErrorInfo = LambdaErrorInfo & {
|
|
22
|
+
s3Location: string;
|
|
23
|
+
explanation: string | null;
|
|
24
|
+
};
|
|
@@ -20,6 +20,7 @@ const waitForLaunched = () => {
|
|
|
20
20
|
});
|
|
21
21
|
};
|
|
22
22
|
const getBrowserInstance = async (shouldDumpIo, chromiumOptions) => {
|
|
23
|
+
var _a;
|
|
23
24
|
if (launching) {
|
|
24
25
|
await waitForLaunched();
|
|
25
26
|
if (!_browserInstance) {
|
|
@@ -33,8 +34,9 @@ const getBrowserInstance = async (shouldDumpIo, chromiumOptions) => {
|
|
|
33
34
|
launching = true;
|
|
34
35
|
const execPath = await (0, get_chromium_executable_path_1.executablePath)();
|
|
35
36
|
const actualChromiumOptions = {
|
|
36
|
-
gl: 'swiftshader',
|
|
37
37
|
...chromiumOptions,
|
|
38
|
+
// Override the `null` value, which might come from CLI with swANGLE
|
|
39
|
+
gl: (_a = chromiumOptions.gl) !== null && _a !== void 0 ? _a : 'swangle',
|
|
38
40
|
};
|
|
39
41
|
_browserInstance = await (0, renderer_1.openBrowser)('chrome', {
|
|
40
42
|
browserExecutable: execPath,
|
|
@@ -3,13 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getFilesToDelete = void 0;
|
|
4
4
|
const constants_1 = require("../../shared/constants");
|
|
5
5
|
const getFilesToDelete = ({ chunkCount, renderId, }) => {
|
|
6
|
-
const chunks = new Array(chunkCount).fill(true).map((
|
|
6
|
+
const chunks = new Array(chunkCount).fill(true).map((_x, i) => (0, constants_1.chunkKeyForIndex)({
|
|
7
7
|
index: i,
|
|
8
8
|
renderId,
|
|
9
9
|
}));
|
|
10
10
|
const lambdaTimings = new Array(chunkCount)
|
|
11
11
|
.fill(true)
|
|
12
|
-
.map((
|
|
12
|
+
.map((_x, i) => (0, constants_1.lambdaTimingsPrefixForChunk)(renderId, i));
|
|
13
13
|
return [
|
|
14
14
|
{
|
|
15
15
|
name: (0, constants_1.lambdaInitializedPrefix)(renderId),
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getProgress = void 0;
|
|
4
4
|
const remotion_1 = require("remotion");
|
|
5
5
|
const constants_1 = require("../../shared/constants");
|
|
6
|
+
const docs_url_1 = require("../../shared/docs-url");
|
|
6
7
|
const calculate_chunk_times_1 = require("./calculate-chunk-times");
|
|
7
8
|
const calculate_price_from_bucket_1 = require("./calculate-price-from-bucket");
|
|
8
9
|
const expected_out_name_1 = require("./expected-out-name");
|
|
@@ -22,7 +23,7 @@ const get_time_to_finish_1 = require("./get-time-to-finish");
|
|
|
22
23
|
const inspect_errors_1 = require("./inspect-errors");
|
|
23
24
|
const io_1 = require("./io");
|
|
24
25
|
const getProgress = async ({ bucketName, renderId, expectedBucketOwner, region, memorySizeInMb, timeoutInMiliseconds, }) => {
|
|
25
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
26
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
26
27
|
const postRenderData = await (0, get_post_render_data_1.getPostRenderData)({
|
|
27
28
|
bucketName,
|
|
28
29
|
region,
|
|
@@ -67,6 +68,7 @@ const getProgress = async ({ bucketName, renderId, expectedBucketOwner, region,
|
|
|
67
68
|
retriesInfo: postRenderData.retriesInfo,
|
|
68
69
|
outKey: outData.key,
|
|
69
70
|
outBucket: outData.renderBucketName,
|
|
71
|
+
mostExpensiveFrameRanges: (_a = postRenderData.mostExpensiveFrameRanges) !== null && _a !== void 0 ? _a : null,
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
const contents = await (0, io_1.lambdaLs)({
|
|
@@ -113,27 +115,27 @@ const getProgress = async ({ bucketName, renderId, expectedBucketOwner, region,
|
|
|
113
115
|
memorySizeInMb,
|
|
114
116
|
outputFileMetadata: outputFile,
|
|
115
117
|
architecture: (0, get_current_architecture_1.getCurrentArchitecture)(),
|
|
116
|
-
lambdasInvoked: (
|
|
118
|
+
lambdasInvoked: (_b = renderMetadata === null || renderMetadata === void 0 ? void 0 : renderMetadata.estimatedRenderLambdaInvokations) !== null && _b !== void 0 ? _b : 0,
|
|
117
119
|
// We cannot determine the ephemeral storage size, so we
|
|
118
120
|
// overestimate the price, but will only have a miniscule effect (~0.2%)
|
|
119
121
|
diskSizeInMb: constants_1.MAX_EPHEMERAL_STORAGE_IN_MB,
|
|
120
122
|
}));
|
|
121
123
|
const cleanup = (0, get_cleanup_progress_1.getCleanupProgress)({
|
|
122
|
-
chunkCount: (
|
|
124
|
+
chunkCount: (_c = renderMetadata === null || renderMetadata === void 0 ? void 0 : renderMetadata.totalChunks) !== null && _c !== void 0 ? _c : 0,
|
|
123
125
|
contents,
|
|
124
|
-
output: (
|
|
126
|
+
output: (_d = outputFile === null || outputFile === void 0 ? void 0 : outputFile.url) !== null && _d !== void 0 ? _d : null,
|
|
125
127
|
renderId,
|
|
126
128
|
});
|
|
127
129
|
const timeToFinish = (0, get_time_to_finish_1.getTimeToFinish)({
|
|
128
|
-
lastModified: (
|
|
130
|
+
lastModified: (_e = outputFile === null || outputFile === void 0 ? void 0 : outputFile.lastModified) !== null && _e !== void 0 ? _e : null,
|
|
129
131
|
renderMetadata,
|
|
130
132
|
});
|
|
131
133
|
const chunks = contents.filter((c) => { var _a; return (_a = c.Key) === null || _a === void 0 ? void 0 : _a.startsWith((0, constants_1.chunkKey)(renderId)); });
|
|
132
|
-
const allChunks = chunks.length === ((
|
|
134
|
+
const allChunks = chunks.length === ((_f = renderMetadata === null || renderMetadata === void 0 ? void 0 : renderMetadata.totalChunks) !== null && _f !== void 0 ? _f : Infinity);
|
|
133
135
|
const renderSize = contents
|
|
134
136
|
.map((c) => { var _a; return (_a = c.Size) !== null && _a !== void 0 ? _a : 0; })
|
|
135
137
|
.reduce((a, b) => a + b, 0);
|
|
136
|
-
const lambdasInvokedStats = (0, get_lambdas_invoked_stats_1.getLambdasInvokedStats)(contents, renderId, (
|
|
138
|
+
const lambdasInvokedStats = (0, get_lambdas_invoked_stats_1.getLambdasInvokedStats)(contents, renderId, (_g = renderMetadata === null || renderMetadata === void 0 ? void 0 : renderMetadata.estimatedRenderLambdaInvokations) !== null && _g !== void 0 ? _g : null, (_h = renderMetadata === null || renderMetadata === void 0 ? void 0 : renderMetadata.startedDate) !== null && _h !== void 0 ? _h : null);
|
|
137
139
|
const retriesInfo = (0, get_retry_stats_1.getRetryStats)({
|
|
138
140
|
contents,
|
|
139
141
|
renderId,
|
|
@@ -145,7 +147,7 @@ const getProgress = async ({ bucketName, renderId, expectedBucketOwner, region,
|
|
|
145
147
|
lambdaInvokeStatus: lambdasInvokedStats,
|
|
146
148
|
});
|
|
147
149
|
const chunkCount = outputFile
|
|
148
|
-
? (
|
|
150
|
+
? (_j = renderMetadata === null || renderMetadata === void 0 ? void 0 : renderMetadata.totalChunks) !== null && _j !== void 0 ? _j : 0
|
|
149
151
|
: chunks.length;
|
|
150
152
|
// We add a 20 second buffer for it, since AWS timeshifts can be quite a lot. Once it's 20sec over the limit, we consider it timed out
|
|
151
153
|
const isBeyondTimeout = renderMetadata &&
|
|
@@ -155,7 +157,7 @@ const getProgress = async ({ bucketName, renderId, expectedBucketOwner, region,
|
|
|
155
157
|
? {
|
|
156
158
|
attempt: 1,
|
|
157
159
|
chunk: null,
|
|
158
|
-
explanation: `The main function timed out after ${timeoutInMiliseconds}ms. Consider increasing the timeout of your function
|
|
160
|
+
explanation: `The main function timed out after ${timeoutInMiliseconds}ms. Consider increasing the timeout of your function. You can use the "--timeout" parameter when deploying a function via CLI, or the "timeoutInSeconds" parameter when using the deployFunction API. ${docs_url_1.DOCS_URL}/docs/lambda/cli/functions#deploy`,
|
|
159
161
|
frame: null,
|
|
160
162
|
isFatal: true,
|
|
161
163
|
s3Location: '',
|
|
@@ -176,7 +178,7 @@ const getProgress = async ({ bucketName, renderId, expectedBucketOwner, region,
|
|
|
176
178
|
renderId,
|
|
177
179
|
renderMetadata,
|
|
178
180
|
bucket: bucketName,
|
|
179
|
-
outputFile: (
|
|
181
|
+
outputFile: (_k = outputFile === null || outputFile === void 0 ? void 0 : outputFile.url) !== null && _k !== void 0 ? _k : null,
|
|
180
182
|
timeToFinish,
|
|
181
183
|
errors: allErrors,
|
|
182
184
|
fatalErrorEncountered: allErrors.some((f) => f.isFatal && !f.willRetry),
|
|
@@ -191,7 +193,7 @@ const getProgress = async ({ bucketName, renderId, expectedBucketOwner, region,
|
|
|
191
193
|
type: 'absolute-time',
|
|
192
194
|
})
|
|
193
195
|
: null,
|
|
194
|
-
timeToInvokeLambdas: (
|
|
196
|
+
timeToInvokeLambdas: (_l = encodingStatus === null || encodingStatus === void 0 ? void 0 : encodingStatus.timeToInvoke) !== null && _l !== void 0 ? _l : lambdasInvokedStats.timeToInvokeLambdas,
|
|
195
197
|
overallProgress: (0, get_overall_progress_1.getOverallProgress)({
|
|
196
198
|
cleanup: cleanup ? cleanup.filesDeleted / cleanup.minFilesToDelete : 0,
|
|
197
199
|
encoding: finalEncodingStatus && renderMetadata
|
|
@@ -211,6 +213,7 @@ const getProgress = async ({ bucketName, renderId, expectedBucketOwner, region,
|
|
|
211
213
|
outBucket: outputFile && renderMetadata
|
|
212
214
|
? (0, expected_out_name_1.getExpectedOutName)(renderMetadata, bucketName).renderBucketName
|
|
213
215
|
: null,
|
|
216
|
+
mostExpensiveFrameRanges: null,
|
|
214
217
|
};
|
|
215
218
|
};
|
|
216
219
|
exports.getProgress = getProgress;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.lambdaReadFile = exports.lambdaWriteFile = exports.lambdaLs = void 0;
|
|
4
7
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
8
|
+
const mime_types_1 = __importDefault(require("mime-types"));
|
|
5
9
|
const aws_clients_1 = require("../../shared/aws-clients");
|
|
6
10
|
const lambdaLs = async ({ bucketName, prefix, region, expectedBucketOwner, continuationToken, }) => {
|
|
7
11
|
var _a, _b, _c;
|
|
@@ -49,6 +53,7 @@ const lambdaWriteFile = async ({ bucketName, key, body, region, privacy, expecte
|
|
|
49
53
|
Body: body,
|
|
50
54
|
ACL: privacy === 'private' ? 'private' : 'public-read',
|
|
51
55
|
ExpectedBucketOwner: expectedBucketOwner !== null && expectedBucketOwner !== void 0 ? expectedBucketOwner : undefined,
|
|
56
|
+
ContentType: mime_types_1.default.lookup(key) || 'application/octet-stream',
|
|
52
57
|
}));
|
|
53
58
|
};
|
|
54
59
|
exports.lambdaWriteFile = lambdaWriteFile;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { openBrowser } from '@remotion/renderer';
|
|
2
|
-
import { TCompMetadata } from 'remotion';
|
|
1
|
+
import { ChromiumOptions, openBrowser } from '@remotion/renderer';
|
|
2
|
+
import { FfmpegExecutable, TCompMetadata } from 'remotion';
|
|
3
3
|
import { Await } from '../../shared/await';
|
|
4
4
|
declare type ValidateCompositionOptions = {
|
|
5
5
|
serveUrl: string;
|
|
@@ -7,6 +7,11 @@ declare type ValidateCompositionOptions = {
|
|
|
7
7
|
browserInstance: Await<ReturnType<typeof openBrowser>>;
|
|
8
8
|
inputProps: unknown;
|
|
9
9
|
envVariables: Record<string, string> | undefined;
|
|
10
|
+
ffmpegExecutable: FfmpegExecutable;
|
|
11
|
+
ffprobeExecutable: FfmpegExecutable;
|
|
12
|
+
timeoutInMilliseconds: number;
|
|
13
|
+
chromiumOptions: ChromiumOptions;
|
|
14
|
+
port: number | null;
|
|
10
15
|
};
|
|
11
|
-
export declare const validateComposition: ({ serveUrl, composition, browserInstance, inputProps, envVariables, }: ValidateCompositionOptions) => Promise<TCompMetadata>;
|
|
16
|
+
export declare const validateComposition: ({ serveUrl, composition, browserInstance, inputProps, envVariables, timeoutInMilliseconds, ffmpegExecutable, ffprobeExecutable, chromiumOptions, port, }: ValidateCompositionOptions) => Promise<TCompMetadata>;
|
|
12
17
|
export {};
|