diff --git a/packages/cli/src/lambda-command.ts b/packages/cli/src/lambda-command.ts index be7a563c3f5..4a6127d70ec 100644 --- a/packages/cli/src/lambda-command.ts +++ b/packages/cli/src/lambda-command.ts @@ -13,7 +13,13 @@ export const lambdaCommand = async ( }); const {LambdaInternals} = require(path); - await LambdaInternals.executeCommand(args, remotionRoot, logLevel, null); + await LambdaInternals.executeCommand( + args, + remotionRoot, + logLevel, + null, + null, + ); process.exit(0); } catch (err) { const manager = StudioServerInternals.getPackageManager( diff --git a/packages/docs/docs/webcodecs/can-reencode-audio-track.mdx b/packages/docs/docs/webcodecs/can-reencode-audio-track.mdx index ab04e1e4246..34845c24da5 100644 --- a/packages/docs/docs/webcodecs/can-reencode-audio-track.mdx +++ b/packages/docs/docs/webcodecs/can-reencode-audio-track.mdx @@ -25,14 +25,14 @@ You can obtain an `AudioTrack` using [`parseMedia()`](/docs/media-parser/parse-m import {parseMedia} from '@remotion/media-parser'; import {canReencodeAudioTrack} from '@remotion/webcodecs'; -const {audioTracks} = await parseMedia({ +const {tracks} = await parseMedia({ src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', fields: { tracks: true, }, }); -for (const track of audioTracks) { +for (const track of tracks.audioTracks) { await canReencodeAudioTrack({ track, audioCodec: 'opus', diff --git a/packages/lambda/package.json b/packages/lambda/package.json index 7b845a23424..e69fa7894c9 100644 --- a/packages/lambda/package.json +++ b/packages/lambda/package.json @@ -10,8 +10,8 @@ "scripts": { "formatting": "prettier src --check", "lint": "eslint src", - "testlambda": "vitest src/test/integration --run", - "test": "vitest src/test/unit --run", + "testlambda": "bun test src/test/integration", + "test": "bun test src/test/unit", "make": "tsc -d && bun build.ts", "prepublishOnly": "bun build.ts && bun ensure-version-match.js" }, @@ -45,7 +45,6 @@ "@types/minimist": "1.2.2", "@types/prompt": "^1.1.0", "pureimage": "0.4.13", - "vitest": "0.31.1", "zip-lib": "^0.7.2", "@remotion/eslint-config-internal": "workspace:*", "eslint": "9.14.0" diff --git a/packages/lambda/src/api/__mocks__/clean-items.ts b/packages/lambda/src/api/__mocks__/clean-items.ts deleted file mode 100644 index a35bfa6a742..00000000000 --- a/packages/lambda/src/api/__mocks__/clean-items.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {mockDeleteS3File} from '../../test/mocks/mock-store'; -import type {cleanItems as original} from '../clean-items'; - -export const cleanItems: typeof original = (input) => { - for (const item of input.list) { - input.onBeforeItemDeleted?.({ - bucketName: input.bucket, - itemName: item, - }); - mockDeleteS3File({ - key: item, - bucketName: input.bucket, - region: input.region, - }); - input.onAfterItemDeleted?.({ - bucketName: input.bucket, - itemName: item, - }); - } - - return Promise.resolve([]); -}; diff --git a/packages/lambda/src/api/__mocks__/create-function.ts b/packages/lambda/src/api/__mocks__/create-function.ts deleted file mode 100644 index c48dba7772c..00000000000 --- a/packages/lambda/src/api/__mocks__/create-function.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {VERSION} from 'remotion/version'; -import { - DEFAULT_EPHEMERAL_STORAGE_IN_MB, - DEFAULT_MEMORY_SIZE, -} from '../../defaults'; -import type {createFunction as original} from '../create-function'; -import {addFunction} from '../mock-functions'; - -export const createFunction: typeof original = (input) => { - if (!input.alreadyCreated) { - addFunction( - { - functionName: input.functionName, - memorySizeInMb: DEFAULT_MEMORY_SIZE, - timeoutInSeconds: input.timeoutInSeconds, - version: VERSION, - diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, - }, - input.region, - ); - } - - return Promise.resolve({ - FunctionName: input.functionName, - }); -}; diff --git a/packages/lambda/src/api/__mocks__/delete-function.ts b/packages/lambda/src/api/__mocks__/delete-function.ts deleted file mode 100644 index 1f36693407a..00000000000 --- a/packages/lambda/src/api/__mocks__/delete-function.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type {deleteFunction as original} from '../delete-function'; -import {deleteMockFunction} from '../mock-functions'; - -export const deleteFunction: typeof original = ({region, functionName}) => { - deleteMockFunction(functionName, region); - - return Promise.resolve(); -}; diff --git a/packages/lambda/src/api/__mocks__/get-functions.ts b/packages/lambda/src/api/__mocks__/get-functions.ts deleted file mode 100644 index 3e232d68899..00000000000 --- a/packages/lambda/src/api/__mocks__/get-functions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {VERSION} from 'remotion/version'; -import type {getFunctions as original} from '../get-functions'; -import {getAllMockFunctions} from '../mock-functions'; - -export const getFunctions: typeof original = ({region, compatibleOnly}) => { - return Promise.resolve( - getAllMockFunctions(region, compatibleOnly ? VERSION : null), - ); -}; diff --git a/packages/lambda/src/api/delete-function.ts b/packages/lambda/src/api/delete-function.ts index 6a9d4674210..6ca61c2a6cb 100644 --- a/packages/lambda/src/api/delete-function.ts +++ b/packages/lambda/src/api/delete-function.ts @@ -1,17 +1,18 @@ import {DeleteFunctionCommand} from '@aws-sdk/client-lambda'; -import type {AwsRegion} from '../regions'; +import type { + DeleteFunction, + DeleteFunctionInput as GenericDeleteFunctionInput, +} from '@remotion/serverless'; +import type {AwsProvider} from '../functions/aws-implementation'; import {getLambdaClient} from '../shared/aws-clients'; -export type DeleteFunctionInput = { - region: AwsRegion; - functionName: string; -}; +export type DeleteFunctionInput = GenericDeleteFunctionInput; /* * @description Deletes a deployed Lambda function based on its name. * @see [Documentation](https://remotion.dev/docs/lambda/deletefunction) */ -export const deleteFunction = async ({ +export const deleteFunction: DeleteFunction = async ({ region, functionName, }: DeleteFunctionInput): Promise => { diff --git a/packages/lambda/src/api/delete-render.ts b/packages/lambda/src/api/delete-render.ts index 0b3fa533f4f..a846dfde744 100644 --- a/packages/lambda/src/api/delete-render.ts +++ b/packages/lambda/src/api/delete-render.ts @@ -8,7 +8,6 @@ import { import type {AwsProvider} from '../functions/aws-implementation'; import {awsImplementation} from '../functions/aws-implementation'; import type {AwsRegion} from '../regions'; -import {getAccountId} from '../shared/get-account-id'; import {cleanItems} from './clean-items'; export type DeleteRenderInput = { @@ -25,7 +24,7 @@ export const internalDeleteRender = async ( forcePathStyle: boolean; }, ) => { - const expectedBucketOwner = await getAccountId({ + const expectedBucketOwner = await input.providerSpecifics.getAccountId({ region: input.region, }); const progress = await getOverallProgressFromStorage({ diff --git a/packages/lambda/src/api/delete-site.ts b/packages/lambda/src/api/delete-site.ts index 0b05a0d5f0a..e765684817a 100644 --- a/packages/lambda/src/api/delete-site.ts +++ b/packages/lambda/src/api/delete-site.ts @@ -3,7 +3,6 @@ import {getSitesKey} from '../defaults'; import type {AwsProvider} from '../functions/aws-implementation'; import {awsImplementation} from '../functions/aws-implementation'; import type {AwsRegion} from '../regions'; -import {getAccountId} from '../shared/get-account-id'; import {cleanItems} from './clean-items'; type MandatoryParameters = { @@ -37,7 +36,7 @@ export const internalDeleteSite = async ({ }: DeleteSiteInput & { providerSpecifics: ProviderSpecifics; }): Promise => { - const accountId = await getAccountId({region}); + const accountId = await providerSpecifics.getAccountId({region}); let files = await providerSpecifics.listObjects({ bucketName, diff --git a/packages/lambda/src/api/deploy-function.ts b/packages/lambda/src/api/deploy-function.ts index 94bfde0984c..dbe68a64955 100644 --- a/packages/lambda/src/api/deploy-function.ts +++ b/packages/lambda/src/api/deploy-function.ts @@ -1,27 +1,30 @@ import type {LogLevel} from '@remotion/renderer'; import {wrapWithErrorHandling} from '@remotion/renderer/error-handling'; +import type { + CloudProvider, + FullClientSpecifics, + ProviderSpecifics, +} from '@remotion/serverless'; import {VERSION} from 'remotion/version'; -import {getFunctions} from '../api/get-functions'; +import {awsImplementation} from '../functions/aws-implementation'; +import {awsFullClientSpecifics} from '../functions/full-client-implementation'; import type {AwsRegion} from '../regions'; import { DEFAULT_CLOUDWATCH_RETENTION_PERIOD, DEFAULT_EPHEMERAL_STORAGE_IN_MB, - RENDER_FN_PREFIX, } from '../shared/constants'; import {FUNCTION_ZIP_ARM64} from '../shared/function-zip-path'; -import {getAccountId} from '../shared/get-account-id'; import { validateRuntimePreference, type RuntimePreference, } from '../shared/get-layers'; -import {LAMBDA_VERSION_STRING} from '../shared/lambda-version-string'; import {validateAwsRegion} from '../shared/validate-aws-region'; import {validateCustomRoleArn} from '../shared/validate-custom-role-arn'; import {validateDiskSizeInMb} from '../shared/validate-disk-size-in-mb'; import {validateMemorySize} from '../shared/validate-memory-size'; import {validateCloudWatchRetentionPeriod} from '../shared/validate-retention-period'; import {validateTimeout} from '../shared/validate-timeout'; -import {createFunction} from './create-function'; +import {speculateFunctionName} from './speculate-function-name'; type MandatoryParameters = { createCloudWatchLogGroup: boolean; @@ -50,8 +53,12 @@ export type DeployFunctionOutput = { alreadyExisted: boolean; }; -export const internalDeployFunction = async ( - params: MandatoryParameters & OptionalParameters, +export const internalDeployFunction = async ( + params: MandatoryParameters & + OptionalParameters & { + providerSpecifics: ProviderSpecifics; + fullClientSpecifics: FullClientSpecifics; + }, ): Promise => { validateMemorySize(params.memorySizeInMb); validateTimeout(params.timeoutInSeconds); @@ -61,15 +68,16 @@ export const internalDeployFunction = async ( validateCustomRoleArn(params.customRoleArn); validateRuntimePreference(params.runtimePreference); - const fnNameRender = [ - `${RENDER_FN_PREFIX}${LAMBDA_VERSION_STRING}`, - `mem${params.memorySizeInMb}mb`, - `disk${params.diskSizeInMb}mb`, - `${params.timeoutInSeconds}sec`, - ].join('-'); - const accountId = await getAccountId({region: params.region}); + const functionName = speculateFunctionName({ + diskSizeInMb: params.diskSizeInMb, + memorySizeInMb: params.memorySizeInMb, + timeoutInSeconds: params.timeoutInSeconds, + }); + const accountId = await params.providerSpecifics.getAccountId({ + region: params.region, + }); - const fns = await getFunctions({ + const fns = await params.providerSpecifics.getFunctions({ compatibleOnly: true, region: params.region, }); @@ -82,11 +90,11 @@ export const internalDeployFunction = async ( f.diskSizeInMb === params.diskSizeInMb, ); - const created = await createFunction({ + const created = await params.fullClientSpecifics.createFunction({ createCloudWatchLogGroup: params.createCloudWatchLogGroup, region: params.region, zipFile: FUNCTION_ZIP_ARM64, - functionName: fnNameRender, + functionName, accountId, memorySizeInMb: params.memorySizeInMb, timeoutInSeconds: params.timeoutInSeconds, @@ -158,5 +166,7 @@ export const deployFunction = ({ vpcSubnetIds, vpcSecurityGroupIds, runtimePreference: runtimePreference ?? 'default', + providerSpecifics: awsImplementation, + fullClientSpecifics: awsFullClientSpecifics, }); }; diff --git a/packages/lambda/src/api/deploy-site.ts b/packages/lambda/src/api/deploy-site.ts index db812f860bf..b548253ad5b 100644 --- a/packages/lambda/src/api/deploy-site.ts +++ b/packages/lambda/src/api/deploy-site.ts @@ -2,21 +2,22 @@ import {type GitSource, type WebpackOverrideFn} from '@remotion/bundler'; import type {ToOptions} from '@remotion/renderer'; import type {BrowserSafeApis} from '@remotion/renderer/client'; import {wrapWithErrorHandling} from '@remotion/renderer/error-handling'; -import type {ProviderSpecifics} from '@remotion/serverless'; +import type { + FullClientSpecifics, + ProviderSpecifics, + UploadDirProgress, +} from '@remotion/serverless'; import {validateBucketName, validatePrivacy} from '@remotion/serverless/client'; import fs from 'node:fs'; import type {AwsProvider} from '../functions/aws-implementation'; import {awsImplementation} from '../functions/aws-implementation'; +import {awsFullClientSpecifics} from '../functions/full-client-implementation'; import type {AwsRegion} from '../regions'; -import {bundleSite} from '../shared/bundle-site'; import {getSitesKey} from '../shared/constants'; -import {getAccountId} from '../shared/get-account-id'; import {getS3DiffOperations} from '../shared/get-s3-operations'; import {makeS3ServeUrl} from '../shared/make-s3-url'; import {validateAwsRegion} from '../shared/validate-aws-region'; import {validateSiteName} from '../shared/validate-site-name'; -import type {UploadDirProgress} from './upload-dir'; -import {uploadDir} from './upload-dir'; type MandatoryParameters = { entryPoint: string; @@ -66,9 +67,11 @@ const mandatoryDeploySite = async ({ throwIfSiteExists, providerSpecifics, forcePathStyle, + fullClientSpecifics, }: MandatoryParameters & OptionalParameters & { providerSpecifics: ProviderSpecifics; + fullClientSpecifics: FullClientSpecifics; }): DeploySiteOutput => { validateAwsRegion(region); validateBucketName(bucketName, { @@ -78,7 +81,7 @@ const mandatoryDeploySite = async ({ validateSiteName(siteName); validatePrivacy(privacy, false); - const accountId = await getAccountId({region}); + const accountId = await providerSpecifics.getAccountId({region}); const bucketExists = await providerSpecifics.bucketExists({ bucketName, @@ -101,7 +104,7 @@ const mandatoryDeploySite = async ({ prefix: `${subFolder}/`, forcePathStyle, }), - bundleSite({ + fullClientSpecifics.bundleSite({ publicPath: `/${subFolder}/`, webpackOverride: options?.webpackOverride ?? ((f) => f), enableCaching: options?.enableCaching ?? true, @@ -142,12 +145,13 @@ const mandatoryDeploySite = async ({ totalBytes = bytes; options.onDiffingProgress?.(bytes, false); }, + fullClientSpecifics, }); options.onDiffingProgress?.(totalBytes, true); await Promise.all([ - uploadDir({ + fullClientSpecifics.uploadDir({ bucket: bucketName, region, localDir: bundled, @@ -170,11 +174,9 @@ const mandatoryDeploySite = async ({ ), ]); - if (!process.env.VITEST) { - fs.rmSync(bundled, { - recursive: true, - }); - } + fs.rmSync(bundled, { + recursive: true, + }); return { serveUrl: makeS3ServeUrl({bucketName, subFolder, region}), @@ -207,5 +209,6 @@ export const deploySite = (args: DeploySiteInput) => { throwIfSiteExists: args.throwIfSiteExists ?? false, providerSpecifics: awsImplementation, forcePathStyle: args.forcePathStyle ?? false, + fullClientSpecifics: awsFullClientSpecifics, }); }; diff --git a/packages/lambda/src/api/download-media.ts b/packages/lambda/src/api/download-media.ts index 181e701a00f..94492bf8acc 100644 --- a/packages/lambda/src/api/download-media.ts +++ b/packages/lambda/src/api/download-media.ts @@ -12,7 +12,6 @@ import {awsImplementation} from '../functions/aws-implementation'; import type {LambdaReadFileProgress} from '../functions/helpers/read-with-progress'; import {lambdaDownloadFileWithProgress} from '../functions/helpers/read-with-progress'; import type {AwsRegion} from '../regions'; -import {getAccountId} from '../shared/get-account-id'; export type DownloadMediaInput = { region: AwsRegion; @@ -36,7 +35,7 @@ export const internalDownloadMedia = async ( forcePathStyle: boolean; }, ): Promise => { - const expectedBucketOwner = await getAccountId({ + const expectedBucketOwner = await input.providerSpecifics.getAccountId({ region: input.region, }); const overallProgress = await getOverallProgressFromStorage({ diff --git a/packages/lambda/src/api/get-compositions-on-lambda.ts b/packages/lambda/src/api/get-compositions-on-lambda.ts index 56de2b353bd..5063e273478 100644 --- a/packages/lambda/src/api/get-compositions-on-lambda.ts +++ b/packages/lambda/src/api/get-compositions-on-lambda.ts @@ -55,10 +55,14 @@ export const getCompositionsOnLambda = async ({ region, userSpecifiedBucketName: bucketName ?? null, propsType: 'input-props', - needsToUpload: getNeedsToUpload('video-or-audio', [ - stringifiedInputProps.length, - JSON.stringify(envVariables).length, - ]), + needsToUpload: getNeedsToUpload({ + type: 'video-or-audio', + sizes: [ + stringifiedInputProps.length, + JSON.stringify(envVariables).length, + ], + providerSpecifics: awsImplementation, + }), providerSpecifics: awsImplementation, forcePathStyle: forcePathStyle ?? false, skipPutAcl: false, diff --git a/packages/lambda/src/api/get-function-info.ts b/packages/lambda/src/api/get-function-info.ts index f1f3f63196e..2c1f55803f3 100644 --- a/packages/lambda/src/api/get-function-info.ts +++ b/packages/lambda/src/api/get-function-info.ts @@ -1,19 +1,12 @@ import {GetFunctionCommand} from '@aws-sdk/client-lambda'; import type {LogLevel} from '@remotion/renderer'; +import type {FunctionInfo} from '@remotion/serverless'; import type {AwsRegion} from '../regions'; import {getLambdaClient} from '../shared/aws-clients'; import {DEFAULT_EPHEMERAL_STORAGE_IN_MB} from '../shared/constants'; import {getFunctionVersion} from '../shared/get-function-version'; import {validateAwsRegion} from '../shared/validate-aws-region'; -export type FunctionInfo = { - functionName: string; - timeoutInSeconds: number; - memorySizeInMb: number; - version: string | null; - diskSizeInMb: number; -}; - export type GetFunctionInfoInput = { region: AwsRegion; functionName: string; diff --git a/packages/lambda/src/api/get-functions.ts b/packages/lambda/src/api/get-functions.ts index 4c9494ae9f7..a1f13c68620 100644 --- a/packages/lambda/src/api/get-functions.ts +++ b/packages/lambda/src/api/get-functions.ts @@ -1,6 +1,7 @@ import type {FunctionConfiguration} from '@aws-sdk/client-lambda'; import {ListFunctionsCommand} from '@aws-sdk/client-lambda'; import type {LogLevel} from '@remotion/renderer'; +import type {FunctionInfo} from '@remotion/serverless'; import {VERSION} from 'remotion/version'; import type {AwsRegion} from '../regions'; import {getLambdaClient} from '../shared/aws-clients'; @@ -9,7 +10,6 @@ import { RENDER_FN_PREFIX, } from '../shared/constants'; import {getFunctionVersion} from '../shared/get-function-version'; -import type {FunctionInfo} from './get-function-info'; export type GetFunctionsInput = { region: AwsRegion; diff --git a/packages/lambda/src/api/get-sites.ts b/packages/lambda/src/api/get-sites.ts index 4db149067c1..03aa9f34ae3 100644 --- a/packages/lambda/src/api/get-sites.ts +++ b/packages/lambda/src/api/get-sites.ts @@ -3,7 +3,6 @@ import type {AwsProvider} from '../functions/aws-implementation'; import {awsImplementation} from '../functions/aws-implementation'; import type {AwsRegion} from '../regions'; import {getSitesKey} from '../shared/constants'; -import {getAccountId} from '../shared/get-account-id'; import {makeS3ServeUrl} from '../shared/make-s3-url'; import type {BucketWithLocation} from './get-buckets'; @@ -51,7 +50,7 @@ export const internalGetSites = async ({ forceBucketName: null, forcePathStyle, }); - const accountId = await getAccountId({region}); + const accountId = await providerSpecifics.getAccountId({region}); const sites: {[key: string]: Site} = {}; diff --git a/packages/lambda/src/api/make-lambda-payload.ts b/packages/lambda/src/api/make-lambda-payload.ts index 8afd2aae11e..687adfbdbbe 100644 --- a/packages/lambda/src/api/make-lambda-payload.ts +++ b/packages/lambda/src/api/make-lambda-payload.ts @@ -138,7 +138,7 @@ export const makeLambdaRenderMediaPayload = async ({ const actualCodec = validateLambdaCodec(codec); validateServeUrl(serveUrl); validateFramesPerFunction({ - framesPerLambda: framesPerLambda ?? null, + framesPerFunction: framesPerLambda ?? null, durationInFrames: 1, }); validateDownloadBehavior(downloadBehavior); @@ -152,10 +152,14 @@ export const makeLambdaRenderMediaPayload = async ({ const serialized = await compressInputProps({ stringifiedInputProps, region, - needsToUpload: getNeedsToUpload('video-or-audio', [ - stringifiedInputProps.length, - JSON.stringify(envVariables).length, - ]), + needsToUpload: getNeedsToUpload({ + type: 'video-or-audio', + sizes: [ + stringifiedInputProps.length, + JSON.stringify(envVariables).length, + ], + providerSpecifics: awsImplementation, + }), userSpecifiedBucketName: bucketName ?? null, propsType: 'input-props', providerSpecifics: awsImplementation, @@ -266,10 +270,14 @@ export const makeLambdaRenderStillPayload = async ({ const serializedInputProps = await compressInputProps({ stringifiedInputProps, region, - needsToUpload: getNeedsToUpload('still', [ - stringifiedInputProps.length, - JSON.stringify(envVariables).length, - ]), + needsToUpload: getNeedsToUpload({ + type: 'still', + sizes: [ + stringifiedInputProps.length, + JSON.stringify(envVariables).length, + ], + providerSpecifics: awsImplementation, + }), userSpecifiedBucketName: forceBucketName ?? null, propsType: 'input-props', providerSpecifics: awsImplementation, diff --git a/packages/lambda/src/api/render-still-on-lambda.ts b/packages/lambda/src/api/render-still-on-lambda.ts index 7f6f77d06d2..6beaf09e9c4 100644 --- a/packages/lambda/src/api/render-still-on-lambda.ts +++ b/packages/lambda/src/api/render-still-on-lambda.ts @@ -11,7 +11,7 @@ import {wrapWithErrorHandling} from '@remotion/renderer/error-handling'; import type { CostsInfo, ReceivedArtifact, - RenderStillLambdaResponsePayload, + RenderStillFunctionResponsePayload, } from '@remotion/serverless'; import type {OutNameInput, Privacy} from '@remotion/serverless/client'; import { @@ -87,7 +87,7 @@ const internalRenderStillOnLambda = async ( try { const payload = await makeLambdaRenderStillPayload(input); const res = await new Promise< - RenderStillLambdaResponsePayload + RenderStillFunctionResponsePayload >((resolve, reject) => { awsImplementation .callFunctionStreaming({ diff --git a/packages/lambda/src/api/upload-dir.ts b/packages/lambda/src/api/upload-dir.ts index 37cd2fc55a7..a8d886ecd8a 100644 --- a/packages/lambda/src/api/upload-dir.ts +++ b/packages/lambda/src/api/upload-dir.ts @@ -1,4 +1,5 @@ import {Upload} from '@aws-sdk/lib-storage'; +import type {UploadDirProgress} from '@remotion/serverless'; import type {Privacy} from '@remotion/serverless/client'; import mimeTypes from 'mime-types'; import type {Dirent} from 'node:fs'; @@ -14,24 +15,11 @@ type FileInfo = { size: number; }; -export type UploadDirProgress = { - totalFiles: number; - filesUploaded: number; - totalSize: number; - sizeUploaded: number; -}; - export type MockFile = { name: string; content: string; }; -export const getDirFiles = (entry: string): MockFile[] => { - throw new TypeError( - 'should only be executed in test ' + JSON.stringify(entry), - ); -}; - async function getFiles( directory: string, originalDirectory: string, diff --git a/packages/lambda/src/cli/commands/compositions/index.ts b/packages/lambda/src/cli/commands/compositions/index.ts index b785da36b17..cfca3e58df7 100644 --- a/packages/lambda/src/cli/commands/compositions/index.ts +++ b/packages/lambda/src/cli/commands/compositions/index.ts @@ -1,7 +1,9 @@ import {CliInternals} from '@remotion/cli'; import type {ChromiumOptions, LogLevel} from '@remotion/renderer'; import {BrowserSafeApis} from '@remotion/renderer/client'; +import {ProviderSpecifics} from '@remotion/serverless'; import {getCompositionsOnLambda} from '../../..'; +import {AwsProvider} from '../../../functions/aws-implementation'; import {BINARY_NAME} from '../../../shared/constants'; import {validateServeUrl} from '../../../shared/validate-serveurl'; import {parsedLambdaCli} from '../../args'; @@ -19,10 +21,15 @@ const { headlessOption, } = BrowserSafeApis.options; -export const compositionsCommand = async ( - args: string[], - logLevel: LogLevel, -) => { +export const compositionsCommand = async ({ + args, + logLevel, + providerSpecifics, +}: { + args: string[]; + logLevel: LogLevel; + providerSpecifics: ProviderSpecifics; +}) => { const serveUrl = args[0]; if (!serveUrl) { @@ -73,7 +80,7 @@ export const compositionsCommand = async ( const region = getAwsRegion(); validateServeUrl(serveUrl); - const functionName = await findFunctionName(logLevel); + const functionName = await findFunctionName({logLevel, providerSpecifics}); const comps = await getCompositionsOnLambda({ functionName, diff --git a/packages/lambda/src/cli/commands/functions/deploy.ts b/packages/lambda/src/cli/commands/functions/deploy.ts index 6d9396cbaf7..690ec3eb9e6 100644 --- a/packages/lambda/src/cli/commands/functions/deploy.ts +++ b/packages/lambda/src/cli/commands/functions/deploy.ts @@ -1,7 +1,9 @@ import {CliInternals} from '@remotion/cli'; import type {LogLevel} from '@remotion/renderer'; +import {FullClientSpecifics, ProviderSpecifics} from '@remotion/serverless'; import {VERSION} from 'remotion/version'; import {internalDeployFunction} from '../../../api/deploy-function'; +import {AwsProvider} from '../../../functions/aws-implementation'; import { DEFAULT_CLOUDWATCH_RETENTION_PERIOD, DEFAULT_EPHEMERAL_STORAGE_IN_MB, @@ -20,7 +22,15 @@ import {Log} from '../../log'; export const FUNCTIONS_DEPLOY_SUBCOMMAND = 'deploy'; -export const functionsDeploySubcommand = async (logLevel: LogLevel) => { +export const functionsDeploySubcommand = async ({ + logLevel, + providerSpecifics, + fullClientSpecifics, +}: { + logLevel: LogLevel; + providerSpecifics: ProviderSpecifics; + fullClientSpecifics: FullClientSpecifics; +}) => { const region = getAwsRegion(); const timeoutInSeconds = parsedLambdaCli.timeout ?? DEFAULT_TIMEOUT; const memorySizeInMb = parsedLambdaCli.memory ?? DEFAULT_MEMORY_SIZE; @@ -99,6 +109,8 @@ VPC Security Group IDs = ${vpcSecurityGroupIds} vpcSubnetIds, vpcSecurityGroupIds, runtimePreference, + providerSpecifics, + fullClientSpecifics, }); if (CliInternals.quietFlagProvided()) { CliInternals.Log.info({indent: false, logLevel}, functionName); diff --git a/packages/lambda/src/cli/commands/functions/index.ts b/packages/lambda/src/cli/commands/functions/index.ts index 665ad8924c2..e9dedaa9f51 100644 --- a/packages/lambda/src/cli/commands/functions/index.ts +++ b/packages/lambda/src/cli/commands/functions/index.ts @@ -1,5 +1,7 @@ import {CliInternals} from '@remotion/cli'; import type {LogLevel} from '@remotion/renderer'; +import {FullClientSpecifics, ProviderSpecifics} from '@remotion/serverless'; +import {AwsProvider} from '../../../functions/aws-implementation'; import {BINARY_NAME} from '../../../shared/constants'; import {quit} from '../../helpers/quit'; import {FUNCTIONS_DEPLOY_SUBCOMMAND, functionsDeploySubcommand} from './deploy'; @@ -54,9 +56,19 @@ const printFunctionsHelp = (logLevel: LogLevel) => { ); }; -export const functionsCommand = (args: string[], logLevel: LogLevel) => { +export const functionsCommand = ({ + args, + logLevel, + fullClientSpecifics, + providerSpecifics, +}: { + args: string[]; + logLevel: LogLevel; + providerSpecifics: ProviderSpecifics; + fullClientSpecifics: FullClientSpecifics; +}) => { if (args[0] === FUNCTIONS_LS_SUBCOMMAND) { - return functionsLsCommand(logLevel); + return functionsLsCommand({logLevel, providerSpecifics}); } if (args[0] === FUNCTIONS_RM_SUBCOMMAND) { @@ -64,11 +76,15 @@ export const functionsCommand = (args: string[], logLevel: LogLevel) => { } if (args[0] === FUNCTIONS_RMALL_SUBCOMMAND) { - return functionsRmallCommand(logLevel); + return functionsRmallCommand({logLevel, providerSpecifics}); } if (args[0] === FUNCTIONS_DEPLOY_SUBCOMMAND) { - return functionsDeploySubcommand(logLevel); + return functionsDeploySubcommand({ + logLevel, + fullClientSpecifics, + providerSpecifics, + }); } if (args[0]) { diff --git a/packages/lambda/src/cli/commands/functions/ls.ts b/packages/lambda/src/cli/commands/functions/ls.ts index 31fb004e03f..39b547744d3 100644 --- a/packages/lambda/src/cli/commands/functions/ls.ts +++ b/packages/lambda/src/cli/commands/functions/ls.ts @@ -1,6 +1,7 @@ import {CliInternals} from '@remotion/cli'; import type {LogLevel} from '@remotion/renderer'; -import {getFunctions} from '../../../api/get-functions'; +import {ProviderSpecifics} from '@remotion/serverless'; +import {AwsProvider} from '../../../functions/aws-implementation'; import {parsedLambdaCli} from '../../args'; import {getAwsRegion} from '../../get-aws-region'; @@ -12,7 +13,13 @@ const VERSION_COLS = 15; export const FUNCTIONS_LS_SUBCOMMAND = 'ls'; -export const functionsLsCommand = async (logLevel: LogLevel) => { +export const functionsLsCommand = async ({ + logLevel, + providerSpecifics, +}: { + logLevel: LogLevel; + providerSpecifics: ProviderSpecifics; +}) => { const region = getAwsRegion(); const fetchingOutput = CliInternals.createOverwriteableCliOutput({ quiet: CliInternals.quietFlagProvided(), @@ -26,7 +33,7 @@ export const functionsLsCommand = async (logLevel: LogLevel) => { const compatibleOnly = parsedLambdaCli['compatible-only'] || false; - const functions = await getFunctions({ + const functions = await providerSpecifics.getFunctions({ region, compatibleOnly, }); diff --git a/packages/lambda/src/cli/commands/functions/rmall.ts b/packages/lambda/src/cli/commands/functions/rmall.ts index 65b490e4a56..03da144671f 100644 --- a/packages/lambda/src/cli/commands/functions/rmall.ts +++ b/packages/lambda/src/cli/commands/functions/rmall.ts @@ -1,17 +1,24 @@ import {CliInternals} from '@remotion/cli'; import type {LogLevel} from '@remotion/renderer'; +import {ProviderSpecifics} from '@remotion/serverless'; import {deleteFunction} from '../../../api/delete-function'; import {getFunctionInfo} from '../../../api/get-function-info'; -import {getFunctions} from '../../../api/get-functions'; +import {AwsProvider} from '../../../functions/aws-implementation'; import {getAwsRegion} from '../../get-aws-region'; import {confirmCli} from '../../helpers/confirm'; export const FUNCTIONS_RMALL_SUBCOMMAND = 'rmall'; const LEFT_COL = 16; -export const functionsRmallCommand = async (logLevel: LogLevel) => { +export const functionsRmallCommand = async ({ + logLevel, + providerSpecifics, +}: { + logLevel: LogLevel; + providerSpecifics: ProviderSpecifics; +}) => { const region = getAwsRegion(); - const functions = await getFunctions({ + const functions = await providerSpecifics.getFunctions({ region, compatibleOnly: false, }); diff --git a/packages/lambda/src/cli/commands/render/render.ts b/packages/lambda/src/cli/commands/render/render.ts index a8554d3a6f1..984550e0458 100644 --- a/packages/lambda/src/cli/commands/render/render.ts +++ b/packages/lambda/src/cli/commands/render/render.ts @@ -59,12 +59,17 @@ const { metadataOption, } = BrowserSafeApis.options; -export const renderCommand = async ( - args: string[], - remotionRoot: string, - logLevel: LogLevel, - implementation: ProviderSpecifics, -) => { +export const renderCommand = async ({ + args, + remotionRoot, + logLevel, + providerSpecifics, +}: { + args: string[]; + remotionRoot: string; + logLevel: LogLevel; + providerSpecifics: ProviderSpecifics; +}) => { const serveUrl = args[0]; if (!serveUrl) { Log.error({indent: false, logLevel}, 'No serve URL passed.'); @@ -264,7 +269,7 @@ export const renderCommand = async ( uiImageFormat: null, }); - const functionName = await findFunctionName(logLevel); + const functionName = await findFunctionName({logLevel, providerSpecifics}); const maxRetries = parsedLambdaCli['max-retries'] ?? DEFAULT_MAX_RETRIES; validateMaxRetries(maxRetries); @@ -272,7 +277,10 @@ export const renderCommand = async ( const privacy = parsedLambdaCli.privacy ?? DEFAULT_OUTPUT_PRIVACY; validatePrivacy(privacy, true); const framesPerLambda = parsedLambdaCli['frames-per-lambda'] ?? undefined; - validateFramesPerFunction({framesPerLambda, durationInFrames: 1}); + validateFramesPerFunction({ + framesPerFunction: framesPerLambda, + durationInFrames: 1, + }); const webhookCustomData = getWebhookCustomData(logLevel); @@ -478,7 +486,7 @@ export const renderCommand = async ( false, ); }, - providerSpecifics: implementation, + providerSpecifics: providerSpecifics, forcePathStyle: parsedLambdaCli['force-path-style'], }); downloadOrNothing = download; diff --git a/packages/lambda/src/cli/commands/sites/create.ts b/packages/lambda/src/cli/commands/sites/create.ts index f10ca8dee26..917440ade85 100644 --- a/packages/lambda/src/cli/commands/sites/create.ts +++ b/packages/lambda/src/cli/commands/sites/create.ts @@ -10,6 +10,7 @@ import { } from '@remotion/serverless/client'; import {NoReactInternals} from 'remotion/no-react'; import type {AwsProvider} from '../../../functions/aws-implementation'; +import {awsFullClientSpecifics} from '../../../functions/full-client-implementation'; import {LambdaInternals} from '../../../internals'; import {BINARY_NAME} from '../../../shared/constants'; import {validateSiteName} from '../../../shared/validate-site-name'; @@ -216,6 +217,7 @@ export const sitesCreateSubcommand = async ( throwIfSiteExists, providerSpecifics: implementation, forcePathStyle: parsedLambdaCli['force-path-style'] ?? false, + fullClientSpecifics: awsFullClientSpecifics, }); const uploadDuration = Date.now() - uploadStart; diff --git a/packages/lambda/src/cli/commands/sites/index.ts b/packages/lambda/src/cli/commands/sites/index.ts index 3abd574b841..3f60f613a2d 100644 --- a/packages/lambda/src/cli/commands/sites/index.ts +++ b/packages/lambda/src/cli/commands/sites/index.ts @@ -60,18 +60,18 @@ export const sitesCommand = ( args: string[], remotionRoot: string, logLevel: LogLevel, - implementation: ProviderSpecifics, + providerSpecifics: ProviderSpecifics, ) => { if (args[0] === SITES_LS_SUBCOMMAND) { return sitesLsSubcommand(logLevel); } if (args[0] === SITES_RM_COMMAND) { - return sitesRmSubcommand(args.slice(1), logLevel, implementation); + return sitesRmSubcommand(args.slice(1), logLevel, providerSpecifics); } if (args[0] === SITES_RMALL_COMMAND) { - return sitesRmallSubcommand(logLevel, implementation); + return sitesRmallSubcommand(logLevel, providerSpecifics); } if (args[0] === SITES_CREATE_SUBCOMMAND) { @@ -79,7 +79,7 @@ export const sitesCommand = ( args.slice(1), remotionRoot, logLevel, - implementation, + providerSpecifics, ); } diff --git a/packages/lambda/src/cli/commands/still.ts b/packages/lambda/src/cli/commands/still.ts index 6a82e37356d..7810a4109cc 100644 --- a/packages/lambda/src/cli/commands/still.ts +++ b/packages/lambda/src/cli/commands/still.ts @@ -52,12 +52,12 @@ export const stillCommand = async ({ args, remotionRoot, logLevel, - implementation, + providerSpecifics, }: { args: string[]; remotionRoot: string; logLevel: LogLevel; - implementation: ProviderSpecifics; + providerSpecifics: ProviderSpecifics; }) => { const serveUrl = args[0]; @@ -186,7 +186,7 @@ export const stillCommand = async ({ const downloadName = args[2] ?? null; const outName = parsedLambdaCli['out-name']; - const functionName = await findFunctionName(logLevel); + const functionName = await findFunctionName({logLevel, providerSpecifics}); const maxRetries = parsedLambdaCli['max-retries'] ?? DEFAULT_MAX_RETRIES; validateMaxRetries(maxRetries); @@ -310,7 +310,7 @@ export const stillCommand = async ({ region, renderId: res.renderId, logLevel, - providerSpecifics: implementation, + providerSpecifics: providerSpecifics, forcePathStyle: parsedLambdaCli['force-path-style'], }); const relativePath = path.relative(process.cwd(), outputPath); diff --git a/packages/lambda/src/cli/get-aws-region.ts b/packages/lambda/src/cli/get-aws-region.ts index 0e2b58fdda1..7cc20851a26 100644 --- a/packages/lambda/src/cli/get-aws-region.ts +++ b/packages/lambda/src/cli/get-aws-region.ts @@ -1,14 +1,9 @@ import type {AwsRegion} from '../regions'; import {DEFAULT_REGION} from '../shared/constants'; -import {isInsideLambda} from '../shared/is-in-lambda'; import {validateAwsRegion} from '../shared/validate-aws-region'; import {parsedLambdaCli} from './args'; export const getAwsRegion = (): AwsRegion => { - if (isInsideLambda()) { - throw new Error('Should not call getAwsRegion() if in lambda'); - } - if (parsedLambdaCli.region) { validateAwsRegion(parsedLambdaCli.region); return parsedLambdaCli.region; diff --git a/packages/lambda/src/cli/helpers/__mocks__/quit.ts b/packages/lambda/src/cli/helpers/__mocks__/quit.ts deleted file mode 100644 index ef01c69959b..00000000000 --- a/packages/lambda/src/cli/helpers/__mocks__/quit.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const quit = (exitCode: number) => { - throw new Error(`Exited process with code ${exitCode}`); -}; diff --git a/packages/lambda/src/cli/helpers/find-function-name.ts b/packages/lambda/src/cli/helpers/find-function-name.ts index 59109e84408..e02381ab9d4 100644 --- a/packages/lambda/src/cli/helpers/find-function-name.ts +++ b/packages/lambda/src/cli/helpers/find-function-name.ts @@ -1,7 +1,8 @@ import type {LogLevel, LogOptions} from '@remotion/renderer'; +import {ProviderSpecifics} from '@remotion/serverless'; import {VERSION} from 'remotion/version'; -import {getFunctions} from '../../api/get-functions'; import {speculateFunctionName} from '../../api/speculate-function-name'; +import {AwsProvider} from '../../functions/aws-implementation'; import {BINARY_NAME} from '../../shared/constants'; import {parsedLambdaCli} from '../args'; import {FUNCTIONS_COMMAND} from '../commands/functions'; @@ -12,8 +13,14 @@ import {getAwsRegion} from '../get-aws-region'; import {Log} from '../log'; import {quit} from './quit'; -export const findFunctionName = async (logLevel: LogLevel) => { - const remotionLambdas = await getFunctions({ +export const findFunctionName = async ({ + logLevel, + providerSpecifics, +}: { + logLevel: LogLevel; + providerSpecifics: ProviderSpecifics; +}) => { + const remotionLambdas = await providerSpecifics.getFunctions({ region: getAwsRegion(), compatibleOnly: false, }); diff --git a/packages/lambda/src/cli/index.ts b/packages/lambda/src/cli/index.ts index f36a44e22a2..9ee416151f0 100644 --- a/packages/lambda/src/cli/index.ts +++ b/packages/lambda/src/cli/index.ts @@ -1,13 +1,16 @@ import {CliInternals} from '@remotion/cli'; import type {LogLevel} from '@remotion/renderer'; import {RenderInternals} from '@remotion/renderer'; -import {type ProviderSpecifics} from '@remotion/serverless'; +import { + FullClientSpecifics, + type ProviderSpecifics, +} from '@remotion/serverless'; import {DOCS_URL} from '@remotion/serverless/client'; import {ROLE_NAME} from '../api/iam-validation/suggested-policy'; import {BINARY_NAME} from '../defaults'; import type {AwsProvider} from '../functions/aws-implementation'; import {awsImplementation} from '../functions/aws-implementation'; -import {checkCredentials} from '../shared/check-credentials'; +import {awsFullClientSpecifics} from '../functions/full-client-implementation'; import {parsedLambdaCli} from './args'; import { COMPOSITIONS_COMMAND, @@ -45,23 +48,35 @@ const requiresCredentials = (args: string[]) => { return true; }; -const matchCommand = ( - args: string[], - remotionRoot: string, - logLevel: LogLevel, - implementation: ProviderSpecifics, -) => { +const matchCommand = ({ + args, + remotionRoot, + logLevel, + providerSpecifics, + fullClientSpecifics, +}: { + args: string[]; + remotionRoot: string; + logLevel: LogLevel; + providerSpecifics: ProviderSpecifics; + fullClientSpecifics: FullClientSpecifics; +}) => { if (parsedLambdaCli.help || args.length === 0) { printHelp(logLevel); quit(0); } if (requiresCredentials(args)) { - checkCredentials(); + fullClientSpecifics.checkCredentials(); } if (args[0] === RENDER_COMMAND) { - return renderCommand(args.slice(1), remotionRoot, logLevel, implementation); + return renderCommand({ + args: args.slice(1), + remotionRoot, + logLevel, + providerSpecifics, + }); } if (args[0] === STILL_COMMAND) { @@ -69,16 +84,25 @@ const matchCommand = ( args: args.slice(1), remotionRoot, logLevel, - implementation, + providerSpecifics: providerSpecifics, }); } if (args[0] === COMPOSITIONS_COMMAND) { - return compositionsCommand(args.slice(1), logLevel); + return compositionsCommand({ + args: args.slice(1), + logLevel, + providerSpecifics, + }); } if (args[0] === FUNCTIONS_COMMAND) { - return functionsCommand(args.slice(1), logLevel); + return functionsCommand({ + args: args.slice(1), + logLevel, + fullClientSpecifics, + providerSpecifics, + }); } if (args[0] === QUOTAS_COMMAND) { @@ -94,7 +118,12 @@ const matchCommand = ( } if (args[0] === SITES_COMMAND) { - return sitesCommand(args.slice(1), remotionRoot, logLevel, implementation); + return sitesCommand( + args.slice(1), + remotionRoot, + logLevel, + providerSpecifics, + ); } if (args[0] === 'upload') { @@ -150,16 +179,18 @@ export const executeCommand = async ( args: string[], remotionRoot: string, logLevel: LogLevel, - implementation: ProviderSpecifics | null, + providerSpecifics: ProviderSpecifics | null, + fullClientSpecifics: FullClientSpecifics | null, ) => { try { setIsCli(true); - await matchCommand( + await matchCommand({ args, remotionRoot, logLevel, - implementation ?? awsImplementation, - ); + providerSpecifics: providerSpecifics ?? awsImplementation, + fullClientSpecifics: fullClientSpecifics ?? awsFullClientSpecifics, + }); } catch (err) { const error = err as Error; if ( @@ -267,5 +298,6 @@ export const cli = async (logLevel: LogLevel) => { remotionRoot, logLevel, awsImplementation, + awsFullClientSpecifics, ); }; diff --git a/packages/lambda/src/client.ts b/packages/lambda/src/client.ts index b2f6f5ce051..da235e1736c 100644 --- a/packages/lambda/src/client.ts +++ b/packages/lambda/src/client.ts @@ -26,11 +26,11 @@ import {validateWebhookSignature} from './api/validate-webhook-signature'; import type {RenderProgress} from './shared/constants'; export type {WebhookPayload} from '@remotion/serverless'; -export {CustomCredentials, DeleteAfter} from '@remotion/serverless/client'; +export type {CustomCredentials, DeleteAfter} from '@remotion/serverless/client'; export { getAwsClient, - GetAwsClientInput, - GetAwsClientOutput, + type GetAwsClientInput, + type GetAwsClientOutput, } from './api/get-aws-client'; export type {AwsRegion} from './regions'; export { diff --git a/packages/lambda/src/functions/aws-implementation.ts b/packages/lambda/src/functions/aws-implementation.ts index e919f97e537..5fa25f08ccf 100644 --- a/packages/lambda/src/functions/aws-implementation.ts +++ b/packages/lambda/src/functions/aws-implementation.ts @@ -3,8 +3,10 @@ import {expiryDays} from '@remotion/serverless/client'; import {EventEmitter} from 'node:events'; import {bucketExistsInRegionImplementation} from '../api/bucket-exists'; import {createBucket} from '../api/create-bucket'; +import {deleteFunction} from '../api/delete-function'; import {estimatePrice} from '../api/estimate-price'; import {getRemotionBuckets} from '../api/get-buckets'; +import {getFunctions} from '../api/get-functions'; import {MAX_EPHEMERAL_STORAGE_IN_MB} from '../defaults'; import {lambdaDeleteFileImplementation} from '../io/delete-file'; import {lambdaHeadFileImplementation} from '../io/head-file'; @@ -16,10 +18,12 @@ import {callFunctionAsyncImplementation} from '../shared/call-lambda-async'; import {callFunctionWithStreamingImplementation} from '../shared/call-lambda-streaming'; import {callFunctionSyncImplementation} from '../shared/call-lambda-sync'; import {convertToServeUrlImplementation} from '../shared/convert-to-serve-url'; +import {getAccountIdImplementation} from '../shared/get-account-id'; import { getCloudwatchMethodUrl, getCloudwatchRendererUrl, } from '../shared/get-aws-urls'; +import type {RuntimePreference} from '../shared/get-layers'; import {isFlakyError} from '../shared/is-flaky-error'; import {applyLifeCyleOperation} from '../shared/lifecycle-rules'; import {randomHashImplementation} from '../shared/random-hash'; @@ -53,6 +57,17 @@ export type AwsProvider = { s3Key: string; s3Url: string; }; + creationFunctionOptions: { + createCloudWatchLogGroup: boolean; + accountId: string; + alreadyCreated: boolean; + retentionInDays: number; + customRoleArn: string; + enableLambdaInsights: boolean; + vpcSubnetIds: string; + vpcSecurityGroupIds: string; + runtimePreference: RuntimePreference; + }; }; const validateDeleteAfter = (lifeCycleValue: unknown) => { @@ -104,14 +119,6 @@ export const awsImplementation: ProviderSpecifics = { callFunctionAsync: callFunctionAsyncImplementation, callFunctionStreaming: callFunctionWithStreamingImplementation, callFunctionSync: callFunctionSyncImplementation, - getCurrentFunctionName() { - const name = process.env.AWS_LAMBDA_FUNCTION_NAME; - if (!name) { - throw new Error('Expected AWS_LAMBDA_FUNCTION_NAME to be set'); - } - - return name; - }, getEphemeralStorageForPriceCalculation() { // We cannot determine the ephemeral storage size, so we // overestimate the price, but will only have a miniscule effect (~0.2%) @@ -122,4 +129,10 @@ export const awsImplementation: ProviderSpecifics = { getLoggingUrlForRendererFunction: getCloudwatchRendererUrl, isFlakyError, getOutputUrl: getOutputUrlFromMetadata, + serverStorageProductName: () => 'S3', + getMaxStillInlinePayloadSize: () => 5_000_000, + getMaxNonInlinePayloadSizePerFunction: () => 200_000, + getAccountId: getAccountIdImplementation, + deleteFunction, + getFunctions, }; diff --git a/packages/lambda/src/functions/aws-server-implementation.ts b/packages/lambda/src/functions/aws-server-implementation.ts index da7531c63e8..939af5e7064 100644 --- a/packages/lambda/src/functions/aws-server-implementation.ts +++ b/packages/lambda/src/functions/aws-server-implementation.ts @@ -1,12 +1,28 @@ -import type {ServerProviderSpecifics} from '@remotion/serverless'; +import type {InsideFunctionSpecifics} from '@remotion/serverless'; import { forgetBrowserEventLoopImplementation, getBrowserInstanceImplementation, } from '@remotion/serverless'; +import {deleteTmpDir} from './helpers/clean-tmpdir'; +import {generateRandomHashWithLifeCycleRule} from './helpers/lifecycle'; import {timer} from './helpers/timer'; -export const serverAwsImplementation: ServerProviderSpecifics = { +export const serverAwsImplementation: InsideFunctionSpecifics = { forgetBrowserEventLoop: forgetBrowserEventLoopImplementation, getBrowserInstance: getBrowserInstanceImplementation, timer, + generateRandomId: ({deleteAfter, randomHashFn}) => { + return generateRandomHashWithLifeCycleRule({deleteAfter, randomHashFn}); + }, + deleteTmpDir: () => Promise.resolve(deleteTmpDir()), + getCurrentFunctionName: () => { + if (!process.env.AWS_LAMBDA_FUNCTION_NAME) { + throw new Error('Expected AWS_LAMBDA_FUNCTION_NAME to be set'); + } + + return process.env.AWS_LAMBDA_FUNCTION_NAME; + }, + getCurrentMemorySizeInMb: () => { + return Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE); + }, }; diff --git a/packages/lambda/src/functions/full-client-implementation.ts b/packages/lambda/src/functions/full-client-implementation.ts new file mode 100644 index 00000000000..90948a3052c --- /dev/null +++ b/packages/lambda/src/functions/full-client-implementation.ts @@ -0,0 +1,16 @@ +import {bundle} from '@remotion/bundler'; +import type {FullClientSpecifics} from '@remotion/serverless'; +import {createFunction} from '../api/create-function'; +import {uploadDir} from '../api/upload-dir'; +import {checkCredentials} from '../shared/check-credentials'; +import {readDirectory} from '../shared/read-dir'; +import type {AwsProvider} from './aws-implementation'; + +export const awsFullClientSpecifics: FullClientSpecifics = { + bundleSite: bundle, + id: '__remotion_full_client_specifics', + readDirectory, + uploadDir, + createFunction, + checkCredentials, +}; diff --git a/packages/lambda/src/functions/helpers/clean-tmpdir.ts b/packages/lambda/src/functions/helpers/clean-tmpdir.ts index ebfc9f9ef4c..c918b40425f 100644 --- a/packages/lambda/src/functions/helpers/clean-tmpdir.ts +++ b/packages/lambda/src/functions/helpers/clean-tmpdir.ts @@ -23,7 +23,5 @@ const deleteAllFilesInAFolderRecursively = (path: string) => { }; export const deleteTmpDir = () => { - if (!process.env.VITEST) { - deleteAllFilesInAFolderRecursively('/tmp'); - } + deleteAllFilesInAFolderRecursively('/tmp'); }; diff --git a/packages/lambda/src/functions/helpers/get-current-region.ts b/packages/lambda/src/functions/helpers/get-current-region.ts index 78d92f173d7..35ff36a193e 100644 --- a/packages/lambda/src/functions/helpers/get-current-region.ts +++ b/packages/lambda/src/functions/helpers/get-current-region.ts @@ -1,13 +1,6 @@ import type {AwsRegion} from '../../regions'; -import {isInsideLambda} from '../../shared/is-in-lambda'; export const getCurrentRegionInFunctionImplementation = () => { - if (!isInsideLambda()) { - throw new Error( - 'Should not call getCurrentRegionInFunctionImplementation() if not inside a lambda function', - ); - } - if (!process.env.AWS_REGION) { throw new Error('Expected process.env.AWS_REGION to be defined'); } diff --git a/packages/lambda/src/functions/helpers/lifecycle.ts b/packages/lambda/src/functions/helpers/lifecycle.ts index fa3d94fb7f3..2cda7641d56 100644 --- a/packages/lambda/src/functions/helpers/lifecycle.ts +++ b/packages/lambda/src/functions/helpers/lifecycle.ts @@ -30,9 +30,12 @@ export const getLifeCycleRules = (): LifecycleRule[] => { export const generateRandomHashWithLifeCycleRule = < Provider extends CloudProvider, ->( - deleteAfter: DeleteAfter | null, - providerSpecifics: ProviderSpecifics, -) => { - return [deleteAfter, providerSpecifics.randomHash()].filter(truthy).join('-'); +>({ + deleteAfter, + randomHashFn, +}: { + deleteAfter: DeleteAfter | null; + randomHashFn: ProviderSpecifics['randomHash']; +}) => { + return [deleteAfter, randomHashFn()].filter(truthy).join('-'); }; diff --git a/packages/lambda/src/functions/index.ts b/packages/lambda/src/functions/index.ts index e5dff89388c..da2442ba902 100644 --- a/packages/lambda/src/functions/index.ts +++ b/packages/lambda/src/functions/index.ts @@ -1,379 +1,12 @@ -import {RenderInternals} from '@remotion/renderer'; -import type { - CloudProvider, - OrError, - ProviderSpecifics, - RequestContext, - ResponseStream, - ResponseStreamWriter, - ServerProviderSpecifics, - StreamingPayload, -} from '@remotion/serverless'; -import { - compositionsHandler, - infoHandler, - launchHandler, - progressHandler, - rendererHandler, - setCurrentRequestId, - startHandler, - stillHandler, - stopLeakDetection, - streamWriter, -} from '@remotion/serverless'; +import type {RequestContext, ResponseStream} from '@remotion/serverless'; +import {innerHandler, streamWriter} from '@remotion/serverless'; import type {ServerlessPayload} from '@remotion/serverless/client'; -import { - ServerlessRoutines, - makeStreamPayload, -} from '@remotion/serverless/client'; -import {COMMAND_NOT_FOUND} from '../shared/constants'; import type {AwsProvider} from './aws-implementation'; import {awsImplementation} from './aws-implementation'; import {serverAwsImplementation} from './aws-server-implementation'; -import {deleteTmpDir} from './helpers/clean-tmpdir'; -import {getWarm, setWarm} from './helpers/is-warm'; -import {generateRandomHashWithLifeCycleRule} from './helpers/lifecycle'; -import {printLoggingGrepHelper} from './helpers/print-logging-helper'; import {streamifyResponse} from './helpers/streamify-response'; import {getWebhookClient} from './http-client'; -const innerHandler = async ({ - params, - responseWriter, - context, - providerSpecifics, - serverProviderSpecifics, -}: { - params: ServerlessPayload; - responseWriter: ResponseStreamWriter; - context: RequestContext; - providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; -}): Promise => { - setCurrentRequestId(context.awsRequestId); - process.env.__RESERVED_IS_INSIDE_REMOTION_LAMBDA = 'true'; - const timeoutInMilliseconds = context.getRemainingTimeInMillis(); - - RenderInternals.Log.verbose( - {indent: false, logLevel: params.logLevel}, - 'AWS Request ID:', - context.awsRequestId, - ); - stopLeakDetection(); - if (!context?.invokedFunctionArn) { - throw new Error( - 'Lambda function unexpectedly does not have context.invokedFunctionArn', - ); - } - - deleteTmpDir(); - const isWarm = getWarm(); - setWarm(); - - const currentUserId = context.invokedFunctionArn.split(':')[4]; - if (params.type === ServerlessRoutines.still) { - providerSpecifics.validateDeleteAfter(params.deleteAfter); - const renderId = generateRandomHashWithLifeCycleRule( - params.deleteAfter, - providerSpecifics, - ); - if (providerSpecifics.printLoggingHelper) { - printLoggingGrepHelper( - ServerlessRoutines.still, - { - renderId, - inputProps: JSON.stringify(params.inputProps), - isWarm, - }, - params.logLevel, - ); - } - - try { - await new Promise((resolve, reject) => { - const onStream = async (payload: StreamingPayload) => { - if (!params.streamed) { - if (payload.type !== 'still-rendered') { - throw new Error('Expected still-rendered'); - } - - await responseWriter.write( - Buffer.from(JSON.stringify(payload.payload)), - ); - return; - } - - const message = makeStreamPayload({ - message: payload, - }); - return new Promise((innerResolve, innerReject) => { - responseWriter - .write(message) - .then(() => { - innerResolve(); - }) - .catch((err) => { - reject(err); - innerReject(err); - }); - }); - }; - - if (params.streamed) { - onStream({ - type: 'render-id-determined', - payload: {renderId}, - }); - } - - stillHandler({ - expectedBucketOwner: currentUserId, - params, - renderId, - onStream, - timeoutInMilliseconds, - providerSpecifics, - serverProviderSpecifics, - }) - .then((r) => { - resolve(r); - }) - .catch((err) => { - reject(err); - }); - }); - await responseWriter.end(); - } catch (err) { - console.log({err}); - } - - return; - } - - if (params.type === ServerlessRoutines.start) { - const renderId = generateRandomHashWithLifeCycleRule( - params.deleteAfter, - providerSpecifics, - ); - - if (providerSpecifics.printLoggingHelper) { - printLoggingGrepHelper( - ServerlessRoutines.start, - { - renderId, - inputProps: JSON.stringify(params.inputProps), - isWarm, - }, - params.logLevel, - ); - } - - const response = await startHandler( - params, - { - expectedBucketOwner: currentUserId, - timeoutInMilliseconds, - renderId, - }, - providerSpecifics, - ); - - await responseWriter.write(Buffer.from(JSON.stringify(response))); - await responseWriter.end(); - return; - } - - if (params.type === ServerlessRoutines.launch) { - if (providerSpecifics.printLoggingHelper) { - printLoggingGrepHelper( - ServerlessRoutines.launch, - { - renderId: params.renderId, - inputProps: JSON.stringify(params.inputProps), - isWarm, - }, - params.logLevel, - ); - } - - const response = await launchHandler({ - params, - options: { - expectedBucketOwner: currentUserId, - getRemainingTimeInMillis: context.getRemainingTimeInMillis, - }, - providerSpecifics, - client: getWebhookClient(params.webhook?.url ?? 'http://localhost:3000'), - serverProviderSpecifics, - }); - - await responseWriter.write(Buffer.from(JSON.stringify(response))); - await responseWriter.end(); - return; - } - - if (params.type === ServerlessRoutines.status) { - if (providerSpecifics.printLoggingHelper) { - printLoggingGrepHelper( - ServerlessRoutines.status, - { - renderId: params.renderId, - isWarm, - }, - params.logLevel, - ); - } - - const response = await progressHandler(params, { - expectedBucketOwner: currentUserId, - timeoutInMilliseconds, - retriesRemaining: 2, - providerSpecifics, - }); - - await responseWriter.write(Buffer.from(JSON.stringify(response))); - await responseWriter.end(); - return; - } - - if (params.type === ServerlessRoutines.renderer) { - if (providerSpecifics.printLoggingHelper) { - printLoggingGrepHelper( - ServerlessRoutines.renderer, - { - renderId: params.renderId, - chunk: String(params.chunk), - dumpLogs: String( - RenderInternals.isEqualOrBelowLogLevel(params.logLevel, 'verbose'), - ), - resolvedProps: JSON.stringify(params.resolvedProps), - isWarm, - }, - params.logLevel, - ); - } - - await new Promise((resolve, reject) => { - rendererHandler({ - params, - options: { - expectedBucketOwner: currentUserId, - isWarm, - }, - onStream: (payload) => { - const message = makeStreamPayload({ - message: payload, - }); - - const writeProm = responseWriter.write(message); - - return new Promise((innerResolve, innerReject) => { - writeProm - .then(() => { - innerResolve(); - }) - .catch((err) => { - reject(err); - innerReject(err); - }); - }); - }, - requestContext: context, - providerSpecifics, - serverProviderSpecifics, - }) - .then((res) => { - resolve(res); - }) - .catch((err) => { - reject(err); - }); - }); - - await responseWriter.end(); - - return; - } - - if (params.type === ServerlessRoutines.info) { - if (providerSpecifics.printLoggingHelper) { - printLoggingGrepHelper( - ServerlessRoutines.info, - { - isWarm, - }, - params.logLevel, - ); - } - - const response = await infoHandler(params); - await responseWriter.write(Buffer.from(JSON.stringify(response))); - await responseWriter.end(); - return; - } - - if (params.type === ServerlessRoutines.compositions) { - if (providerSpecifics.printLoggingHelper) { - printLoggingGrepHelper( - ServerlessRoutines.compositions, - { - isWarm, - }, - params.logLevel, - ); - } - - const response = await compositionsHandler( - params, - { - expectedBucketOwner: currentUserId, - }, - providerSpecifics, - serverProviderSpecifics, - ); - - await responseWriter.write(Buffer.from(JSON.stringify(response))); - await responseWriter.end(); - - return; - } - - throw new Error(COMMAND_NOT_FOUND); -}; - -export const innerRoutine = async ({ - params, - responseWriter, - context, - providerSpecifics, - serverProviderSpecifics, -}: { - params: ServerlessPayload; - responseWriter: ResponseStreamWriter; - context: RequestContext; - providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; -}): Promise => { - try { - await innerHandler({ - params, - responseWriter, - context, - providerSpecifics, - serverProviderSpecifics, - }); - } catch (err) { - const res: OrError<0> = { - type: 'error', - message: (err as Error).message, - stack: (err as Error).stack as string, - }; - - await responseWriter.write(Buffer.from(JSON.stringify(res))); - await responseWriter.end(); - } -}; - export const routine = ( params: ServerlessPayload, responseStream: ResponseStream, @@ -381,12 +14,13 @@ export const routine = ( ): Promise => { const responseWriter = streamWriter(responseStream); - return innerRoutine({ + return innerHandler({ params, responseWriter, context, providerSpecifics: awsImplementation, - serverProviderSpecifics: serverAwsImplementation, + insideFunctionSpecifics: serverAwsImplementation, + webhookClient: getWebhookClient, }); }; diff --git a/packages/lambda/src/index.ts b/packages/lambda/src/index.ts index 574d0aa912b..b2369dbcedc 100644 --- a/packages/lambda/src/index.ts +++ b/packages/lambda/src/index.ts @@ -1,4 +1,8 @@ -import type {EnhancedErrorInfo, LambdaErrorInfo} from '@remotion/serverless'; +import type { + EnhancedErrorInfo, + FunctionInfo, + LambdaErrorInfo, +} from '@remotion/serverless'; import type { CustomCredentials, GetOrCreateBucketInput, @@ -32,7 +36,7 @@ import type { GetCompositionsOnLambdaOutput, } from './api/get-compositions-on-lambda'; import {getCompositionsOnLambda} from './api/get-compositions-on-lambda'; -import type {FunctionInfo, GetFunctionInfoInput} from './api/get-function-info'; +import type {GetFunctionInfoInput} from './api/get-function-info'; import {getFunctionInfo} from './api/get-function-info'; import type {GetFunctionsInput} from './api/get-functions'; import {getFunctions} from './api/get-functions'; @@ -68,11 +72,12 @@ import {renderStillOnLambda as deprecatedRenderStillOnLambda} from './api/render import {validateWebhookSignature} from './api/validate-webhook-signature'; import { LambdaInternals, - _InternalAwsProvider, - _InternalOverallRenderProgress, + type _InternalAwsProvider, + type _InternalOverallRenderProgress, } from './internals'; import type {AwsRegion} from './regions'; import type {RenderProgress} from './shared/constants'; + export type {WebhookPayload} from '@remotion/serverless'; /** @@ -131,7 +136,6 @@ const getSites = NoReactInternals.ENABLE_V5_BREAKING_CHANGES : deprecatedGetSites; export { - LambdaInternals, deleteFunction, deleteRender, deleteSite, @@ -149,6 +153,7 @@ export { getRolePolicy, getSites, getUserPolicy, + LambdaInternals, presignUrl, renderMediaOnLambda, renderStillOnLambda, diff --git a/packages/lambda/src/internals.ts b/packages/lambda/src/internals.ts index 9c712ea62c4..00cd542dd69 100644 --- a/packages/lambda/src/internals.ts +++ b/packages/lambda/src/internals.ts @@ -14,5 +14,5 @@ export const LambdaInternals = { internalDeploySite, }; -export {OverallRenderProgress as _InternalOverallRenderProgress} from '@remotion/serverless'; -export {AwsProvider as _InternalAwsProvider} from './functions/aws-implementation'; +export type {OverallRenderProgress as _InternalOverallRenderProgress} from '@remotion/serverless'; +export type {AwsProvider as _InternalAwsProvider} from './functions/aws-implementation'; diff --git a/packages/lambda/src/shared/__mocks__/check-credentials.ts b/packages/lambda/src/shared/__mocks__/check-credentials.ts deleted file mode 100644 index 0e630ef0e3e..00000000000 --- a/packages/lambda/src/shared/__mocks__/check-credentials.ts +++ /dev/null @@ -1 +0,0 @@ -export const checkCredentials = () => undefined; diff --git a/packages/lambda/src/shared/__mocks__/get-account-id.ts b/packages/lambda/src/shared/__mocks__/get-account-id.ts deleted file mode 100644 index 226d4104282..00000000000 --- a/packages/lambda/src/shared/__mocks__/get-account-id.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type {getAccountId as original} from '../get-account-id'; - -export const getAccountId: typeof original = () => { - const accountId = 'aws:iam::123456789'.match(/aws:iam::([0-9]+)/); - if (!accountId) { - throw new Error('Cannot get account ID'); - } - - return Promise.resolve(accountId[1]); -}; diff --git a/packages/lambda/src/shared/constants.ts b/packages/lambda/src/shared/constants.ts index 6c30ace9e4c..5dd0c6cc0e0 100644 --- a/packages/lambda/src/shared/constants.ts +++ b/packages/lambda/src/shared/constants.ts @@ -15,7 +15,6 @@ export const MAX_TIMEOUT = 900; export const DEFAULT_FRAMES_PER_LAMBDA = 20; export const BINARY_NAME = 'remotion lambda'; -export const COMMAND_NOT_FOUND = 'Command not found'; export const DEFAULT_REGION: AwsRegion = 'us-east-1'; export const DEFAULT_MAX_RETRIES = 1; diff --git a/packages/lambda/src/shared/get-account-id.ts b/packages/lambda/src/shared/get-account-id.ts index fea7c106dca..48225bba2ad 100644 --- a/packages/lambda/src/shared/get-account-id.ts +++ b/packages/lambda/src/shared/get-account-id.ts @@ -1,9 +1,13 @@ import {GetCallerIdentityCommand} from '@aws-sdk/client-sts'; +import type {GetAccountId} from '@remotion/serverless'; +import type {AwsProvider} from '../functions/aws-implementation'; import type {AwsRegion} from '../regions'; import {getStsClient} from './aws-clients'; import {validateAwsRegion} from './validate-aws-region'; -export const getAccountId = async (options: {region: AwsRegion}) => { +export const getAccountIdImplementation: GetAccountId< + AwsProvider +> = async (options: {region: AwsRegion}) => { validateAwsRegion(options.region); const callerIdentity = await getStsClient(options.region).send( diff --git a/packages/lambda/src/shared/get-function-version.ts b/packages/lambda/src/shared/get-function-version.ts index d19b13c7bb2..7d31ff97d38 100644 --- a/packages/lambda/src/shared/get-function-version.ts +++ b/packages/lambda/src/shared/get-function-version.ts @@ -1,8 +1,10 @@ import type {LogLevel} from '@remotion/renderer'; -import {ServerlessRoutines} from '@remotion/serverless/client'; +import { + COMMAND_NOT_FOUND, + ServerlessRoutines, +} from '@remotion/serverless/client'; import {awsImplementation} from '../functions/aws-implementation'; import type {AwsRegion} from '../regions'; -import {COMMAND_NOT_FOUND} from './constants'; export const getFunctionVersion = async ({ functionName, diff --git a/packages/lambda/src/shared/get-s3-operations.ts b/packages/lambda/src/shared/get-s3-operations.ts index a63b130bc15..f23a87b8cbc 100644 --- a/packages/lambda/src/shared/get-s3-operations.ts +++ b/packages/lambda/src/shared/get-s3-operations.ts @@ -1,19 +1,22 @@ import type {_Object} from '@aws-sdk/client-s3'; -import {readDirectory} from './read-dir'; +import type {FullClientSpecifics} from '@remotion/serverless'; +import type {AwsProvider} from '../functions/aws-implementation'; export const getS3DiffOperations = async ({ objects, bundle, prefix, onProgress, + fullClientSpecifics, }: { objects: _Object[]; bundle: string; prefix: string; onProgress: (bytes: number) => void; + fullClientSpecifics: FullClientSpecifics; }) => { let totalBytes = 0; - const dir = readDirectory({ + const dir = fullClientSpecifics.readDirectory({ dir: bundle, etags: {}, originalDir: bundle, diff --git a/packages/lambda/src/shared/read-dir.ts b/packages/lambda/src/shared/read-dir.ts index bb481edb5bc..b73fd5b5386 100644 --- a/packages/lambda/src/shared/read-dir.ts +++ b/packages/lambda/src/shared/read-dir.ts @@ -1,20 +1,16 @@ +import type {ReadDir} from '@remotion/serverless'; import * as fs from 'node:fs'; import * as path from 'node:path'; import {getEtagOfFile} from './get-etag'; // Function to recursively read a directory and return a list of files // with their etags and file names -export function readDirectory({ +export const readDirectory: ReadDir = ({ dir, etags, originalDir, onProgress, -}: { - dir: string; - etags: {[key: string]: () => Promise}; - originalDir: string; - onProgress: (bytes: number) => void; -}) { +}) => { const files = fs.readdirSync(dir); for (const file of files) { @@ -52,4 +48,4 @@ export function readDirectory({ // Return the list of files with their etags and file names return etags; -} +}; diff --git a/packages/lambda/src/test/integration/cli.test.ts b/packages/lambda/src/test/integration/cli.test.ts index 3c726bbebd5..bfced0d67a6 100644 --- a/packages/lambda/src/test/integration/cli.test.ts +++ b/packages/lambda/src/test/integration/cli.test.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line no-restricted-imports import {CliInternals} from '@remotion/cli'; -import {expect, test} from 'vitest'; +import {afterEach, beforeEach, expect, test} from 'bun:test'; import { DEFAULT_EPHEMERAL_STORAGE_IN_MB, DEFAULT_MEMORY_SIZE, @@ -8,17 +8,29 @@ import { } from '../../defaults'; import {LambdaInternals} from '../../internals'; import {LAMBDA_VERSION_STRING} from '../../shared/lambda-version-string'; -import {mockImplementation} from '../mock-implementation'; -import {getProcessWriteOutput} from './console-hooks'; +import { + mockFullClientSpecifics, + mockImplementation, +} from '../mock-implementation'; +import {doAfter, doBefore, getProcessWriteOutput} from './console-hooks'; const remotionRoot = process.cwd(); +beforeEach(() => { + doBefore(); +}); + +afterEach(() => { + doAfter(); +}); + test('Deploy function', async () => { await LambdaInternals.executeCommand( ['functions', 'deploy'], remotionRoot, 'verbose', mockImplementation, + mockFullClientSpecifics, ); expect(getProcessWriteOutput()).toContain( `Deployed as remotion-render-${LAMBDA_VERSION_STRING}-mem${DEFAULT_MEMORY_SIZE}mb-disk${DEFAULT_EPHEMERAL_STORAGE_IN_MB}mb-${DEFAULT_TIMEOUT}sec\n`, @@ -31,12 +43,14 @@ test('Deploy function and list it', async () => { remotionRoot, 'info', mockImplementation, + mockFullClientSpecifics, ); await LambdaInternals.executeCommand( ['functions', 'ls'], remotionRoot, 'info', mockImplementation, + mockFullClientSpecifics, ); expect(getProcessWriteOutput()).toContain('Getting functions...'); expect(getProcessWriteOutput()).toContain('Memory (MB)'); @@ -49,12 +63,14 @@ test('Deploy function and it already exists should fail', async () => { remotionRoot, 'info', mockImplementation, + mockFullClientSpecifics, ); await LambdaInternals.executeCommand( ['functions', 'deploy'], remotionRoot, 'info', mockImplementation, + mockFullClientSpecifics, ); expect(getProcessWriteOutput()).toMatch(/Already exists as remotion-render/); @@ -67,6 +83,7 @@ test('If no functions are there and is quiet, should return "()"', async () => { remotionRoot, 'info', mockImplementation, + mockFullClientSpecifics, ); expect(getProcessWriteOutput()).toBe('()'); }); @@ -77,6 +94,7 @@ test('Should handle functions rm called with no functions', async () => { remotionRoot, 'info', mockImplementation, + mockFullClientSpecifics, ); expect(getProcessWriteOutput()).toBe('No functions to remove.'); }); diff --git a/packages/lambda/src/test/integration/console-hooks.ts b/packages/lambda/src/test/integration/console-hooks.ts index 9297e73dd1d..4e8e17e6ce5 100644 --- a/packages/lambda/src/test/integration/console-hooks.ts +++ b/packages/lambda/src/test/integration/console-hooks.ts @@ -1,6 +1,5 @@ import {NoReactInternals} from 'remotion/no-react'; -import {afterEach, beforeEach} from 'vitest'; -import {cleanFnStore} from '../../api/mock-functions'; +import {cleanFnStore} from '../mocks/mock-functions'; let stdoutOutput: string[] = []; let stderrOutput: string[] = []; @@ -17,7 +16,7 @@ const originalConsoleLog = console.log; const originalConsoleError = console.error; const originalStderr = process.stderr.write; -beforeEach(() => { +export const doBefore = () => { stdoutOutput = []; stderrOutput = []; cleanFnStore(); @@ -42,11 +41,11 @@ beforeEach(() => { // originalStderr(str); stderrOutput.push(str); }; -}); +}; -afterEach(() => { +export const doAfter = () => { process.stdout.write = originalStdout; process.stderr.write = originalStderr; console.log = originalConsoleLog; console.error = originalConsoleError; -}); +}; diff --git a/packages/lambda/src/test/integration/delete-site.test.ts b/packages/lambda/src/test/integration/delete-site.test.ts index b8c7f10e3d9..d11c016e7df 100644 --- a/packages/lambda/src/test/integration/delete-site.test.ts +++ b/packages/lambda/src/test/integration/delete-site.test.ts @@ -1,8 +1,11 @@ import {internalGetOrCreateBucket} from '@remotion/serverless/client'; -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {internalDeleteSite} from '../../api/delete-site'; import {internalDeploySite} from '../../api/deploy-site'; -import {mockImplementation} from '../mock-implementation'; +import { + mockFullClientSpecifics, + mockImplementation, +} from '../mock-implementation'; test('Return 0 total size if site did not exist', async () => { const {bucketName} = await internalGetOrCreateBucket({ @@ -46,6 +49,7 @@ test('Return more than 0 total size if site did not exist', async () => { throwIfSiteExists: false, siteName: mockImplementation.randomHash(), forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }); expect( ( diff --git a/packages/lambda/src/test/integration/deploy-function.test.ts b/packages/lambda/src/test/integration/deploy-function.test.ts index 21cdaadc171..cb964b2f336 100644 --- a/packages/lambda/src/test/integration/deploy-function.test.ts +++ b/packages/lambda/src/test/integration/deploy-function.test.ts @@ -1,61 +1,89 @@ +import {expect, test} from 'bun:test'; import {VERSION} from 'remotion/version'; -import {expect, test} from 'vitest'; -import {deleteFunction} from '../../api/delete-function'; -import {deployFunction} from '../../api/deploy-function'; -import {getFunctions} from '../../api/get-functions'; +import {internalDeployFunction} from '../../api/deploy-function'; +import {speculateFunctionName} from '../../api/speculate-function-name'; +import {DEFAULT_EPHEMERAL_STORAGE_IN_MB} from '../../shared/constants'; +import { + mockFullClientSpecifics, + mockImplementation, +} from '../mock-implementation'; import { cleanFnStore, markFunctionAsIncompatible, -} from '../../api/mock-functions'; -import {DEFAULT_EPHEMERAL_STORAGE_IN_MB} from '../../shared/constants'; -import {LAMBDA_VERSION_STRING} from '../../shared/lambda-version-string'; - -const expectedFunctionName = (memory: number, timeout: number, disk: number) => - `remotion-render-${LAMBDA_VERSION_STRING}-mem${memory}mb-disk${disk}mb-${timeout}sec`; +} from '../mocks/mock-functions'; test('Should be able to deploy function', async () => { - const {functionName} = await deployFunction({ + const {functionName} = await internalDeployFunction({ memorySizeInMb: 2048, region: 'us-east-1', timeoutInSeconds: 120, createCloudWatchLogGroup: true, + customRoleArn: undefined, + diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, + enableLambdaInsights: true, + indent: false, + logLevel: 'info', + providerSpecifics: mockImplementation, + fullClientSpecifics: mockFullClientSpecifics, + runtimePreference: 'default', + vpcSecurityGroupIds: undefined, + vpcSubnetIds: undefined, + cloudWatchLogRetentionPeriodInDays: undefined, }); expect(functionName).toBe( - expectedFunctionName(2048, 120, DEFAULT_EPHEMERAL_STORAGE_IN_MB), + speculateFunctionName({ + memorySizeInMb: 2048, + timeoutInSeconds: 120, + diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, + }), ); }); test('Should be able to get the function afterwards', async () => { cleanFnStore(); - const {functionName} = await deployFunction({ + const {functionName} = await internalDeployFunction({ memorySizeInMb: 2048, region: 'us-east-1', timeoutInSeconds: 120, createCloudWatchLogGroup: true, + customRoleArn: undefined, + diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, + enableLambdaInsights: true, + indent: false, + logLevel: 'info', + providerSpecifics: mockImplementation, + fullClientSpecifics: mockFullClientSpecifics, + runtimePreference: 'default', + vpcSecurityGroupIds: undefined, + vpcSubnetIds: undefined, + cloudWatchLogRetentionPeriodInDays: undefined, }); expect(functionName).toBe( - expectedFunctionName(2048, 120, DEFAULT_EPHEMERAL_STORAGE_IN_MB), + speculateFunctionName({ + memorySizeInMb: 2048, + timeoutInSeconds: 120, + diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, + }), ); - const fns = await getFunctions({ + const fns = await mockImplementation.getFunctions({ region: 'us-east-1', compatibleOnly: true, }); expect(fns).toEqual([ { - functionName: expectedFunctionName( - 2048, - 120, - DEFAULT_EPHEMERAL_STORAGE_IN_MB, - ), + functionName: speculateFunctionName({ + memorySizeInMb: 2048, + timeoutInSeconds: 120, + diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, + }), memorySizeInMb: 2048, timeoutInSeconds: 120, version: VERSION, - region: 'us-east-1', - diskSizeInMb: 2048, + diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, }, ]); - const foreignFunctions = await getFunctions({ + const foreignFunctions = await mockImplementation.getFunctions({ region: 'us-east-2', compatibleOnly: true, }); @@ -65,24 +93,39 @@ test('Should be able to get the function afterwards', async () => { test('Should be able to delete the function', async () => { cleanFnStore(); - const {functionName} = await deployFunction({ + const {functionName} = await internalDeployFunction({ memorySizeInMb: 2048, region: 'us-east-1', timeoutInSeconds: 120, createCloudWatchLogGroup: true, + customRoleArn: undefined, + diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, + enableLambdaInsights: true, + indent: false, + logLevel: 'info', + providerSpecifics: mockImplementation, + fullClientSpecifics: mockFullClientSpecifics, + runtimePreference: 'default', + vpcSecurityGroupIds: undefined, + vpcSubnetIds: undefined, + cloudWatchLogRetentionPeriodInDays: undefined, }); expect(functionName).toBe( - expectedFunctionName(2048, 120, DEFAULT_EPHEMERAL_STORAGE_IN_MB), + speculateFunctionName({ + memorySizeInMb: 2048, + timeoutInSeconds: 120, + diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, + }), ); - await deleteFunction({ + mockImplementation.deleteFunction({ region: 'us-east-1', - functionName: expectedFunctionName( - 2048, - 120, - DEFAULT_EPHEMERAL_STORAGE_IN_MB, - ), + functionName: speculateFunctionName({ + memorySizeInMb: 2048, + timeoutInSeconds: 120, + diskSizeInMb: DEFAULT_EPHEMERAL_STORAGE_IN_MB, + }), }); - const fns = await getFunctions({ + const fns = await mockImplementation.getFunctions({ region: 'us-east-1', compatibleOnly: true, }); @@ -92,57 +135,74 @@ test('Should be able to delete the function', async () => { test('Should be able to get the function afterwards', async () => { cleanFnStore(); - const {functionName} = await deployFunction({ + const {functionName} = await internalDeployFunction({ memorySizeInMb: 2048, region: 'us-east-1', timeoutInSeconds: 120, createCloudWatchLogGroup: true, + customRoleArn: undefined, + diskSizeInMb: 10240, + enableLambdaInsights: true, + indent: false, + logLevel: 'info', + providerSpecifics: mockImplementation, + fullClientSpecifics: mockFullClientSpecifics, + runtimePreference: 'default', + vpcSecurityGroupIds: undefined, + vpcSubnetIds: undefined, + cloudWatchLogRetentionPeriodInDays: undefined, }); expect(functionName).toBe( - expectedFunctionName(2048, 120, DEFAULT_EPHEMERAL_STORAGE_IN_MB), + speculateFunctionName({ + memorySizeInMb: 2048, + timeoutInSeconds: 120, + diskSizeInMb: 10240, + }), ); - const fns = await getFunctions({ + const fns = await mockImplementation.getFunctions({ region: 'us-east-1', compatibleOnly: true, }); expect(fns).toEqual([ { - functionName: expectedFunctionName( - 2048, - 120, - DEFAULT_EPHEMERAL_STORAGE_IN_MB, - ), + functionName: speculateFunctionName({ + memorySizeInMb: 2048, + timeoutInSeconds: 120, + diskSizeInMb: 10240, + }), memorySizeInMb: 2048, timeoutInSeconds: 120, version: VERSION, - region: 'us-east-1', - diskSizeInMb: 2048, + diskSizeInMb: 10240, }, ]); markFunctionAsIncompatible( - expectedFunctionName(2048, 120, DEFAULT_EPHEMERAL_STORAGE_IN_MB), + speculateFunctionName({ + memorySizeInMb: 2048, + timeoutInSeconds: 120, + diskSizeInMb: 10240, + }), ); - const compatibleFns = await getFunctions({ + const compatibleFns = await mockImplementation.getFunctions({ region: 'us-east-1', compatibleOnly: true, }); - const incompatibleFns = await getFunctions({ + const incompatibleFns = await mockImplementation.getFunctions({ region: 'us-east-1', compatibleOnly: false, }); expect(compatibleFns).toEqual([]); expect(incompatibleFns).toEqual([ { - functionName: expectedFunctionName( - 2048, - 120, - DEFAULT_EPHEMERAL_STORAGE_IN_MB, - ), + functionName: speculateFunctionName({ + memorySizeInMb: 2048, + timeoutInSeconds: 120, + diskSizeInMb: 10240, + }), memorySizeInMb: 2048, timeoutInSeconds: 120, version: '2021-06-23', - region: 'us-east-1', - diskSizeInMb: 2048, + diskSizeInMb: 10240, }, ]); }); diff --git a/packages/lambda/src/test/integration/deploy-site.test.ts b/packages/lambda/src/test/integration/deploy-site.test.ts index 44f93a95d61..af74850ca4d 100644 --- a/packages/lambda/src/test/integration/deploy-site.test.ts +++ b/packages/lambda/src/test/integration/deploy-site.test.ts @@ -1,12 +1,16 @@ import {internalGetOrCreateBucket} from '@remotion/serverless/client'; -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {internalDeleteSite} from '../../api/delete-site'; import {internalDeploySite} from '../../api/deploy-site'; -import {getDirFiles} from '../../api/upload-dir'; -import {mockImplementation} from '../mock-implementation'; +import {awsImplementation} from '../../functions/aws-implementation'; +import { + mockFullClientSpecifics, + mockImplementation, +} from '../mock-implementation'; +import {getDirFiles} from '../mocks/upload-dir'; test('Should throw on wrong prefix', async () => { - await expect(() => + await expect( internalDeploySite({ bucketName: 'wrongprefix', entryPoint: 'first', @@ -20,12 +24,13 @@ test('Should throw on wrong prefix', async () => { siteName: mockImplementation.randomHash(), throwIfSiteExists: true, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }), ).rejects.toThrow(/The bucketName parameter must start /); }); test('Should throw if invalid region was passed', () => { - expect(() => + expect( internalDeploySite({ bucketName: 'remotionlambda-testing', entryPoint: 'first', @@ -33,7 +38,7 @@ test('Should throw if invalid region was passed', () => { region: 'ap-northeast-9', siteName: 'testing', gitSource: null, - providerSpecifics: mockImplementation, + providerSpecifics: awsImplementation, indent: false, logLevel: 'info', options: {}, @@ -44,7 +49,7 @@ test('Should throw if invalid region was passed', () => { }); test("Should throw if bucket doesn't exist", () => { - expect(() => + expect( internalDeploySite({ bucketName: 'remotionlambda-non-existed', entryPoint: 'first', @@ -58,6 +63,7 @@ test("Should throw if bucket doesn't exist", () => { privacy: 'public', throwIfSiteExists: true, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }), ).rejects.toThrow( /No bucket with the name remotionlambda-non-existed exists/, @@ -87,6 +93,7 @@ test('Should apply name if given', async () => { throwIfSiteExists: true, providerSpecifics: mockImplementation, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }), ).toEqual({ siteName: 'testing', @@ -124,6 +131,7 @@ test('Should overwrite site if given siteName is already taken', async () => { privacy: 'public', throwIfSiteExists: false, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }), ).toEqual({ siteName: 'testing', @@ -160,6 +168,7 @@ test('Should delete the previous site if deploying the new one', async () => { privacy: 'public', throwIfSiteExists: false, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }); await internalDeploySite({ bucketName, @@ -174,6 +183,7 @@ test('Should delete the previous site if deploying the new one', async () => { privacy: 'public', throwIfSiteExists: false, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }); const files = await mockImplementation.listObjects({ @@ -218,6 +228,7 @@ test('Should keep the previous site if deploying the new one with different ID', privacy: 'public', throwIfSiteExists: false, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }); await internalDeploySite({ bucketName, @@ -232,6 +243,7 @@ test('Should keep the previous site if deploying the new one with different ID', privacy: 'public', throwIfSiteExists: false, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }); const files = await mockImplementation.listObjects({ @@ -296,6 +308,7 @@ test('Should not delete site with same prefix', async () => { privacy: 'public', throwIfSiteExists: false, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }); await internalDeploySite({ gitSource: null, @@ -310,6 +323,7 @@ test('Should not delete site with same prefix', async () => { privacy: 'public', throwIfSiteExists: false, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }); await internalDeploySite({ gitSource: null, @@ -324,6 +338,7 @@ test('Should not delete site with same prefix', async () => { privacy: 'public', throwIfSiteExists: false, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }); const files = await mockImplementation.listObjects({ diff --git a/packages/lambda/src/test/integration/get-sites.test.ts b/packages/lambda/src/test/integration/get-sites.test.ts index 4870f04c79f..6e3160c82bb 100644 --- a/packages/lambda/src/test/integration/get-sites.test.ts +++ b/packages/lambda/src/test/integration/get-sites.test.ts @@ -1,8 +1,11 @@ import {internalGetOrCreateBucket} from '@remotion/serverless/client'; -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {internalDeploySite} from '../../api/deploy-site'; import {internalGetSites} from '../../api/get-sites'; -import {mockImplementation} from '../mock-implementation'; +import { + mockFullClientSpecifics, + mockImplementation, +} from '../mock-implementation'; test('Should have no buckets at first', async () => { expect( @@ -38,6 +41,7 @@ test('Should have a site after deploying', async () => { throwIfSiteExists: true, options: {}, forcePathStyle: false, + fullClientSpecifics: mockFullClientSpecifics, }), ).toEqual({ serveUrl: diff --git a/packages/lambda/src/test/integration/handlers.test.ts b/packages/lambda/src/test/integration/handlers.test.ts index 42316151e5d..78ab27539fd 100644 --- a/packages/lambda/src/test/integration/handlers.test.ts +++ b/packages/lambda/src/test/integration/handlers.test.ts @@ -1,6 +1,6 @@ import {ServerlessRoutines} from '@remotion/serverless/client'; +import {expect, test} from 'bun:test'; import {VERSION} from 'remotion/version'; -import {expect, test} from 'vitest'; import {mockImplementation} from '../mock-implementation'; test('Call function locally', async () => { diff --git a/packages/lambda/src/test/integration/lifecycle.test.ts b/packages/lambda/src/test/integration/lifecycle.test.ts index 83641ea2b6f..6bb85c43aeb 100644 --- a/packages/lambda/src/test/integration/lifecycle.test.ts +++ b/packages/lambda/src/test/integration/lifecycle.test.ts @@ -1,4 +1,4 @@ -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {getLifeCycleRules} from '../../functions/helpers/lifecycle'; test('Lifecycle', () => { diff --git a/packages/lambda/src/test/integration/renders/gif.test.ts b/packages/lambda/src/test/integration/renders/gif.test.ts index 5bf502bbfd3..f221ebfb8ec 100644 --- a/packages/lambda/src/test/integration/renders/gif.test.ts +++ b/packages/lambda/src/test/integration/renders/gif.test.ts @@ -1,8 +1,8 @@ import {RenderInternals} from '@remotion/renderer'; +import {afterAll, expect, test} from 'bun:test'; import {createWriteStream, unlinkSync} from 'node:fs'; import {tmpdir} from 'node:os'; import path from 'node:path'; -import {afterAll, expect, test} from 'vitest'; import {simulateLambdaRender} from '../simulate-lambda-render'; afterAll(async () => { diff --git a/packages/lambda/src/test/integration/renders/muted-render.test.ts b/packages/lambda/src/test/integration/renders/muted-render.test.ts index 18f5a2b8294..75a46a59279 100644 --- a/packages/lambda/src/test/integration/renders/muted-render.test.ts +++ b/packages/lambda/src/test/integration/renders/muted-render.test.ts @@ -1,9 +1,9 @@ import {RenderInternals, getVideoMetadata} from '@remotion/renderer'; import {rendersPrefix} from '@remotion/serverless/client'; +import {afterAll, expect, test} from 'bun:test'; import {createWriteStream, unlinkSync} from 'node:fs'; import {tmpdir} from 'node:os'; import path from 'path'; -import {afterAll, expect, test} from 'vitest'; import {internalDeleteRender} from '../../../api/delete-render'; import {mockImplementation} from '../../mock-implementation'; import {simulateLambdaRender} from '../simulate-lambda-render'; diff --git a/packages/lambda/src/test/integration/renders/old-version.test.ts b/packages/lambda/src/test/integration/renders/old-version.test.ts index 13c4ced49b5..7a99e1241ff 100644 --- a/packages/lambda/src/test/integration/renders/old-version.test.ts +++ b/packages/lambda/src/test/integration/renders/old-version.test.ts @@ -1,6 +1,6 @@ import {RenderInternals} from '@remotion/renderer'; import {ServerlessRoutines} from '@remotion/serverless/client'; -import {afterAll, expect, test} from 'vitest'; +import {afterAll, expect, test} from 'bun:test'; import {mockImplementation} from '../../mock-implementation'; afterAll(async () => { @@ -8,8 +8,6 @@ afterAll(async () => { }); test('Should fail when using an incompatible version', async () => { - process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '2048'; - try { const aha = await mockImplementation.callFunctionSync({ type: ServerlessRoutines.launch, @@ -22,7 +20,7 @@ test('Should fail when using an incompatible version', async () => { crf: 9, envVariables: {}, frameRange: [0, 12], - framesPerLambda: 8, + framesPerFunction: 8, imageFormat: 'png', inputProps: { type: 'payload', @@ -40,7 +38,7 @@ test('Should fail when using an incompatible version', async () => { timeoutInMilliseconds: 12000, numberOfGifLoops: null, everyNthFrame: 1, - concurrencyPerLambda: 1, + concurrencyPerFunction: 1, downloadBehavior: { type: 'play-in-browser', }, diff --git a/packages/lambda/src/test/integration/renders/other-bucket.test.ts b/packages/lambda/src/test/integration/renders/other-bucket.test.ts index 389bf6c7d0d..d04b1e4aa51 100644 --- a/packages/lambda/src/test/integration/renders/other-bucket.test.ts +++ b/packages/lambda/src/test/integration/renders/other-bucket.test.ts @@ -1,5 +1,5 @@ import {RenderInternals} from '@remotion/renderer'; -import {afterAll, expect, test} from 'vitest'; +import {afterAll, expect, test} from 'bun:test'; import {simulateLambdaRender} from '../simulate-lambda-render'; afterAll(async () => { diff --git a/packages/lambda/src/test/integration/renders/regular-audio.test.ts b/packages/lambda/src/test/integration/renders/regular-audio.test.ts index e557f596bee..90d74c88578 100644 --- a/packages/lambda/src/test/integration/renders/regular-audio.test.ts +++ b/packages/lambda/src/test/integration/renders/regular-audio.test.ts @@ -1,8 +1,8 @@ import {RenderInternals} from '@remotion/renderer'; import {rendersPrefix} from '@remotion/serverless/client'; +import {afterAll, expect, test} from 'bun:test'; import {createWriteStream, unlinkSync} from 'fs'; import path from 'path'; -import {afterAll, expect, test} from 'vitest'; import {internalDeleteRender} from '../../../api/delete-render'; import {mockImplementation} from '../../mock-implementation'; import {Wavedraw} from '../draw-wav'; @@ -12,72 +12,76 @@ afterAll(async () => { await RenderInternals.killAllBrowsers(); }); -test('Should make regular (non-seamless) audio', async () => { - const {close, file, progress, renderId} = await simulateLambdaRender({ - codec: 'wav', - composition: 'framer', - frameRange: [100, 200], - imageFormat: 'none', - logLevel: 'error', - region: 'eu-central-1', - inputProps: {playbackRate: 2}, - }); +test( + 'Should make regular (non-seamless) audio', + async () => { + const {close, file, progress, renderId} = await simulateLambdaRender({ + codec: 'wav', + composition: 'framer', + frameRange: [100, 200], + imageFormat: 'none', + logLevel: 'error', + region: 'eu-central-1', + inputProps: {playbackRate: 2}, + }); - const wav = path.join(process.cwd(), 'regular.wav'); - await new Promise((resolve) => { - file.pipe(createWriteStream(wav)).on('finish', () => resolve()); - }); + const wav = path.join(process.cwd(), 'regular.wav'); + await new Promise((resolve) => { + file.pipe(createWriteStream(wav)).on('finish', () => resolve()); + }); - const wd = new Wavedraw(wav); + const wd = new Wavedraw(wav); - const snapShot = path.join(__dirname, 'regular-audio.bmp'); + const snapShot = path.join(__dirname, 'regular-audio.bmp'); - const options = { - width: 600, - height: 300, - rms: true, - maximums: true, - average: false, - start: 'START' as const, - end: 'END' as const, - colors: { - maximums: '#0000ff', - rms: '#659df7', - background: '#ffffff', - }, - filename: snapShot, - }; + const options = { + width: 600, + height: 300, + rms: true, + maximums: true, + average: false, + start: 'START' as const, + end: 'END' as const, + colors: { + maximums: '#0000ff', + rms: '#659df7', + background: '#ffffff', + }, + filename: snapShot, + }; - wd.drawWave(options); // outputs wave drawing to example1.png + wd.drawWave(options); // outputs wave drawing to example1.png - const files = await mockImplementation.listObjects({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - expectedBucketOwner: 'abc', - prefix: rendersPrefix(renderId), - forcePathStyle: false, - }); + const files = await mockImplementation.listObjects({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + expectedBucketOwner: 'abc', + prefix: rendersPrefix(renderId), + forcePathStyle: false, + }); - expect(files.length).toBe(2); + expect(files.length).toBe(2); - await internalDeleteRender({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - renderId, - providerSpecifics: mockImplementation, - forcePathStyle: false, - }); + await internalDeleteRender({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + renderId, + providerSpecifics: mockImplementation, + forcePathStyle: false, + }); - const expectFiles = await mockImplementation.listObjects({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - expectedBucketOwner: 'abc', - prefix: rendersPrefix(renderId), - forcePathStyle: false, - }); + const expectFiles = await mockImplementation.listObjects({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + expectedBucketOwner: 'abc', + prefix: rendersPrefix(renderId), + forcePathStyle: false, + }); - expect(expectFiles.length).toBe(0); + expect(expectFiles.length).toBe(0); - unlinkSync(wav); - await close(); -}); + unlinkSync(wav); + await close(); + }, + {timeout: 30000}, +); diff --git a/packages/lambda/src/test/integration/renders/seamless-audio.test.ts b/packages/lambda/src/test/integration/renders/seamless-audio.test.ts index 770f45944ae..f99b757bb4c 100644 --- a/packages/lambda/src/test/integration/renders/seamless-audio.test.ts +++ b/packages/lambda/src/test/integration/renders/seamless-audio.test.ts @@ -1,8 +1,8 @@ import {RenderInternals} from '@remotion/renderer'; import {rendersPrefix} from '@remotion/serverless/client'; +import {afterAll, expect, test} from 'bun:test'; import {existsSync, unlinkSync} from 'fs'; import path from 'path'; -import {afterAll, expect, test} from 'vitest'; import {internalDeleteRender} from '../../../api/delete-render'; import {mockImplementation} from '../../mock-implementation'; import {Wavedraw} from '../draw-wav'; @@ -12,85 +12,89 @@ afterAll(async () => { await RenderInternals.killAllBrowsers(); }); -test('Should make seamless audio', async () => { - const {close, file, progress, renderId} = await simulateLambdaRender({ - codec: 'aac', - composition: 'framer', - frameRange: [100, 200], - imageFormat: 'none', - logLevel: 'error', - region: 'eu-central-1', - inputProps: {playbackRate: 2}, - metadata: {Author: 'Lunar'}, - }); +test( + 'Should make seamless audio', + async () => { + const {close, file, progress, renderId} = await simulateLambdaRender({ + codec: 'aac', + composition: 'framer', + frameRange: [100, 200], + imageFormat: 'none', + logLevel: 'error', + region: 'eu-central-1', + inputProps: {playbackRate: 2}, + metadata: {Author: 'Lunar'}, + }); - const wav = path.join(process.cwd(), 'seamless.wav'); - if (existsSync(wav)) { - unlinkSync(wav); - } + const wav = path.join(process.cwd(), 'seamless.wav'); + if (existsSync(wav)) { + unlinkSync(wav); + } - await RenderInternals.callFf({ - bin: 'ffmpeg', - args: ['-i', '-', '-ac', '1', '-c:a', 'pcm_s16le', '-y', wav], - options: { - stdin: file, - }, - indent: false, - binariesDirectory: null, - cancelSignal: undefined, - logLevel: 'info', - }); + await RenderInternals.callFf({ + bin: 'ffmpeg', + args: ['-i', '-', '-ac', '1', '-c:a', 'pcm_s16le', '-y', wav], + options: { + stdin: file, + }, + indent: false, + binariesDirectory: null, + cancelSignal: undefined, + logLevel: 'info', + }); - const wd = new Wavedraw(wav); + const wd = new Wavedraw(wav); - const snapShot = path.join(__dirname, 'seamless-audio.bmp'); + const snapShot = path.join(__dirname, 'seamless-audio.bmp'); - const options = { - width: 600, - height: 300, - rms: true, - maximums: true, - average: false, - start: 'START' as const, - end: 'END' as const, - colors: { - maximums: '#0000ff', - rms: '#659df7', - background: '#ffffff', - }, - filename: snapShot, - }; + const options = { + width: 600, + height: 300, + rms: true, + maximums: true, + average: false, + start: 'START' as const, + end: 'END' as const, + colors: { + maximums: '#0000ff', + rms: '#659df7', + background: '#ffffff', + }, + filename: snapShot, + }; - await wd.drawWave(options); // outputs wave drawing to example1.png + await wd.drawWave(options); // outputs wave drawing to example1.png - const files = await mockImplementation.listObjects({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - expectedBucketOwner: 'abc', - prefix: rendersPrefix(renderId), - forcePathStyle: false, - }); + const files = await mockImplementation.listObjects({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + expectedBucketOwner: 'abc', + prefix: rendersPrefix(renderId), + forcePathStyle: false, + }); - expect(files.length).toBe(2); + expect(files.length).toBe(2); - await internalDeleteRender({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - renderId, - providerSpecifics: mockImplementation, - forcePathStyle: false, - }); + await internalDeleteRender({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + renderId, + providerSpecifics: mockImplementation, + forcePathStyle: false, + }); - const expectFiles = await mockImplementation.listObjects({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - expectedBucketOwner: 'abc', - prefix: rendersPrefix(renderId), - forcePathStyle: false, - }); + const expectFiles = await mockImplementation.listObjects({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + expectedBucketOwner: 'abc', + prefix: rendersPrefix(renderId), + forcePathStyle: false, + }); - expect(expectFiles.length).toBe(0); + expect(expectFiles.length).toBe(0); - unlinkSync(wav); - await close(); -}); + unlinkSync(wav); + await close(); + }, + {timeout: 30000}, +); diff --git a/packages/lambda/src/test/integration/renders/slow-seamless-audio.test.ts b/packages/lambda/src/test/integration/renders/slow-seamless-audio.test.ts index 829287066a1..121d137a99e 100644 --- a/packages/lambda/src/test/integration/renders/slow-seamless-audio.test.ts +++ b/packages/lambda/src/test/integration/renders/slow-seamless-audio.test.ts @@ -1,8 +1,8 @@ import {RenderInternals} from '@remotion/renderer'; import {rendersPrefix} from '@remotion/serverless/client'; +import {afterAll, expect, test} from 'bun:test'; import {existsSync, unlinkSync} from 'fs'; import path from 'path'; -import {afterAll, expect, test} from 'vitest'; import {internalDeleteRender} from '../../../api/delete-render'; import {mockImplementation} from '../../mock-implementation'; import {Wavedraw} from '../draw-wav'; @@ -12,84 +12,88 @@ afterAll(async () => { await RenderInternals.killAllBrowsers(); }); -test('Should make slowed down seamless audio', async () => { - const {close, file, progress, renderId} = await simulateLambdaRender({ - codec: 'aac', - composition: 'framer', - frameRange: [1700, 1740], - imageFormat: 'none', - logLevel: 'error', - region: 'eu-central-1', - inputProps: {playbackRate: 0.5}, - }); +test( + 'Should make slowed down seamless audio', + async () => { + const {close, file, progress, renderId} = await simulateLambdaRender({ + codec: 'aac', + composition: 'framer', + frameRange: [1700, 1740], + imageFormat: 'none', + logLevel: 'error', + region: 'eu-central-1', + inputProps: {playbackRate: 0.5}, + }); - const wav = path.join(process.cwd(), 'slow-seamless.wav'); - if (existsSync(wav)) { - unlinkSync(wav); - } + const wav = path.join(process.cwd(), 'slow-seamless.wav'); + if (existsSync(wav)) { + unlinkSync(wav); + } - await RenderInternals.callFf({ - bin: 'ffmpeg', - args: ['-i', '-', '-ac', '1', '-c:a', 'pcm_s16le', '-y', wav], - options: { - stdin: file, - }, - indent: false, - binariesDirectory: null, - cancelSignal: undefined, - logLevel: 'info', - }); + await RenderInternals.callFf({ + bin: 'ffmpeg', + args: ['-i', '-', '-ac', '1', '-c:a', 'pcm_s16le', '-y', wav], + options: { + stdin: file, + }, + indent: false, + binariesDirectory: null, + cancelSignal: undefined, + logLevel: 'info', + }); - const wd = new Wavedraw(wav); + const wd = new Wavedraw(wav); - const snapShot = path.join(__dirname, 'slow-seamless-audio.bmp'); + const snapShot = path.join(__dirname, 'slow-seamless-audio.bmp'); - const options = { - width: 600, - height: 300, - rms: true, - maximums: true, - average: false, - start: 'START' as const, - end: 'END' as const, - colors: { - maximums: '#0000ff', - rms: '#659df7', - background: '#ffffff', - }, - filename: snapShot, - }; + const options = { + width: 600, + height: 300, + rms: true, + maximums: true, + average: false, + start: 'START' as const, + end: 'END' as const, + colors: { + maximums: '#0000ff', + rms: '#659df7', + background: '#ffffff', + }, + filename: snapShot, + }; - wd.drawWave(options); // outputs wave drawing to example1.png + wd.drawWave(options); // outputs wave drawing to example1.png - const files = await mockImplementation.listObjects({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - expectedBucketOwner: 'abc', - prefix: rendersPrefix(renderId), - forcePathStyle: false, - }); + const files = await mockImplementation.listObjects({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + expectedBucketOwner: 'abc', + prefix: rendersPrefix(renderId), + forcePathStyle: false, + }); - expect(files.length).toBe(2); + expect(files.length).toBe(2); - await internalDeleteRender({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - renderId, - providerSpecifics: mockImplementation, - forcePathStyle: false, - }); + await internalDeleteRender({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + renderId, + providerSpecifics: mockImplementation, + forcePathStyle: false, + }); - const expectFiles = await mockImplementation.listObjects({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - expectedBucketOwner: 'abc', - prefix: rendersPrefix(renderId), - forcePathStyle: false, - }); + const expectFiles = await mockImplementation.listObjects({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + expectedBucketOwner: 'abc', + prefix: rendersPrefix(renderId), + forcePathStyle: false, + }); - expect(expectFiles.length).toBe(0); + expect(expectFiles.length).toBe(0); - unlinkSync(wav); - await close(); -}); + unlinkSync(wav); + await close(); + }, + {timeout: 30000}, +); diff --git a/packages/lambda/src/test/integration/renders/transparent-webm.test.ts b/packages/lambda/src/test/integration/renders/transparent-webm.test.ts index ab32e012436..c47c2199d2e 100644 --- a/packages/lambda/src/test/integration/renders/transparent-webm.test.ts +++ b/packages/lambda/src/test/integration/renders/transparent-webm.test.ts @@ -1,9 +1,9 @@ import {RenderInternals} from '@remotion/renderer'; import {rendersPrefix} from '@remotion/serverless/client'; +import {afterAll, expect, test} from 'bun:test'; import fs, {createWriteStream} from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import {afterAll, expect, test} from 'vitest'; import {internalDeleteRender} from '../../../api/delete-render'; import {mockImplementation} from '../../mock-implementation'; import {simulateLambdaRender} from '../simulate-lambda-render'; @@ -12,68 +12,74 @@ afterAll(async () => { await RenderInternals.killAllBrowsers(); }); -test('Should make a transparent video', async () => { - const {close, file, progress, renderId} = await simulateLambdaRender({ - codec: 'vp8', - composition: 'ten-frame-tester', - frameRange: [0, 9], - imageFormat: 'png', - framesPerLambda: 5, - logLevel: 'error', - region: 'eu-central-1', - outName: 'out.webm', - pixelFormat: 'yuva420p', - }); +test( + 'Should make a transparent video', + async () => { + const {close, file, progress, renderId} = await simulateLambdaRender({ + codec: 'vp8', + composition: 'ten-frame-tester', + frameRange: [0, 9], + imageFormat: 'png', + framesPerLambda: 5, + logLevel: 'error', + region: 'eu-central-1', + outName: 'out.webm', + pixelFormat: 'yuva420p', + }); - // We create a temporary directory for storing the frames - const tmpdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'remotion-')); - const out = path.join(tmpdir, 'hithere.webm'); - file.pipe(createWriteStream(out)); + // We create a temporary directory for storing the frames + const tmpdir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'remotion-'), + ); + const out = path.join(tmpdir, 'hithere.webm'); + file.pipe(createWriteStream(out)); - await new Promise((resolve) => { - file.on('close', () => resolve()); - }); - const probe = await RenderInternals.callFf({ - bin: 'ffprobe', - args: [out], - indent: false, - logLevel: 'info', - binariesDirectory: null, - cancelSignal: undefined, - }); - expect(probe.stderr).toMatch(/ALPHA_MODE(\s+): 1/); - expect(probe.stderr).toMatch(/Video: vp8, yuv420p/); - expect(probe.stderr).toMatch(/Audio: opus, 48000 Hz/); - fs.unlinkSync(out); + await new Promise((resolve) => { + file.on('close', () => resolve()); + }); + const probe = await RenderInternals.callFf({ + bin: 'ffprobe', + args: [out], + indent: false, + logLevel: 'info', + binariesDirectory: null, + cancelSignal: undefined, + }); + expect(probe.stderr).toMatch(/ALPHA_MODE(\s+): 1/); + expect(probe.stderr).toMatch(/Video: vp8, yuv420p/); + expect(probe.stderr).toMatch(/Audio: opus, 48000 Hz/); + fs.unlinkSync(out); - const files = await mockImplementation.listObjects({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - expectedBucketOwner: 'abc', - prefix: rendersPrefix(renderId), - forcePathStyle: false, - }); + const files = await mockImplementation.listObjects({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + expectedBucketOwner: 'abc', + prefix: rendersPrefix(renderId), + forcePathStyle: false, + }); - expect(files.length).toBe(2); + expect(files.length).toBe(2); - await internalDeleteRender({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - renderId, - providerSpecifics: mockImplementation, - forcePathStyle: false, - }); + await internalDeleteRender({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + renderId, + providerSpecifics: mockImplementation, + forcePathStyle: false, + }); - const expectFiles = await mockImplementation.listObjects({ - bucketName: progress.outBucket as string, - region: 'eu-central-1', - expectedBucketOwner: 'abc', - prefix: rendersPrefix(renderId), - forcePathStyle: false, - }); + const expectFiles = await mockImplementation.listObjects({ + bucketName: progress.outBucket as string, + region: 'eu-central-1', + expectedBucketOwner: 'abc', + prefix: rendersPrefix(renderId), + forcePathStyle: false, + }); - RenderInternals.deleteDirectory(tmpdir); - expect(expectFiles.length).toBe(0); + RenderInternals.deleteDirectory(tmpdir); + expect(expectFiles.length).toBe(0); - await close(); -}); + await close(); + }, + {timeout: 60000}, +); diff --git a/packages/lambda/src/test/integration/webhooks.test.ts b/packages/lambda/src/test/integration/webhooks.test.ts index b4dee223184..ffc3cee8fb1 100644 --- a/packages/lambda/src/test/integration/webhooks.test.ts +++ b/packages/lambda/src/test/integration/webhooks.test.ts @@ -1,15 +1,15 @@ import {RenderInternals, ensureBrowser} from '@remotion/renderer'; import {ServerlessRoutines} from '@remotion/serverless/client'; +import {beforeAll, beforeEach, describe, expect, mock, test} from 'bun:test'; import path from 'path'; import {VERSION} from 'remotion/version'; -import {beforeAll, beforeEach, describe, expect, test, vi} from 'vitest'; import {mockableHttpClients} from '../../functions/http-client'; import {mockImplementation} from '../mock-implementation'; const originalFetch = mockableHttpClients.http; beforeEach(() => { // @ts-expect-error - mockableHttpClients.http = vi.fn( + mockableHttpClients.http = mock( ( _url: string, _options: unknown, @@ -158,8 +158,6 @@ describe('Webhooks', () => { }); test('Should call webhook upon timeout', async () => { - process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '2048'; - const exampleBuild = path.join(process.cwd(), '..', 'example', 'build'); // Maybe this can use simulateLambdaRender instead @@ -189,7 +187,7 @@ describe('Webhooks', () => { crf: 9, envVariables: {}, frameRange: [0, 10], - framesPerLambda: 8, + framesPerFunction: 8, imageFormat: 'png', inputProps: { type: 'payload', @@ -207,7 +205,7 @@ describe('Webhooks', () => { timeoutInMilliseconds: 3000, numberOfGifLoops: null, everyNthFrame: 1, - concurrencyPerLambda: 1, + concurrencyPerFunction: 1, downloadBehavior: { type: 'play-in-browser', }, diff --git a/packages/lambda/src/test/mock-implementation.ts b/packages/lambda/src/test/mock-implementation.ts index f938173cba6..4e6c92dbec9 100644 --- a/packages/lambda/src/test/mock-implementation.ts +++ b/packages/lambda/src/test/mock-implementation.ts @@ -1,8 +1,9 @@ import {openBrowser} from '@remotion/renderer'; import type { + FullClientSpecifics, GetBrowserInstance, + InsideFunctionSpecifics, ProviderSpecifics, - ServerProviderSpecifics, } from '@remotion/serverless'; import {Readable} from 'stream'; import {estimatePrice} from '../api/estimate-price'; @@ -15,11 +16,16 @@ import { getCloudwatchRendererUrl, } from '../shared/get-aws-urls'; import {isFlakyError} from '../shared/is-flaky-error'; +import {randomHashImplementation} from '../shared/random-hash'; import { getMockCallFunctionAsync, getMockCallFunctionStreaming, getMockCallFunctionSync, } from './mocks/aws-clients'; +import {mockBundleSite} from './mocks/mock-bundle-site'; +import {mockCreateFunction} from './mocks/mock-create-function'; +import {deleteMockFunction, getAllMockFunctions} from './mocks/mock-functions'; +import {mockReadDirectory} from './mocks/mock-read-dir'; import { addMockBucket, getMockBuckets, @@ -29,6 +35,7 @@ import { readMockS3File, writeMockS3File, } from './mocks/mock-store'; +import {mockUploadDir} from './mocks/upload-dir'; type Await = T extends PromiseLike ? U : T; let _browserInstance: Await> | null; @@ -39,6 +46,13 @@ export const getBrowserInstance: GetBrowserInstance = async () => { }; export const mockImplementation: ProviderSpecifics = { + getAccountId: () => + Promise.resolve('aws:iam::123456789'.match(/aws:iam::([0-9]+)/)?.[1]!), + getMaxNonInlinePayloadSizePerFunction: () => 200_000, + getMaxStillInlinePayloadSize() { + return 5_000_000; + }, + serverStorageProductName: () => 'file system', applyLifeCycle: () => Promise.resolve(), getChromiumPath() { return null; @@ -55,7 +69,8 @@ export const mockImplementation: ProviderSpecifics = { }); return Promise.resolve(); }, - getBuckets: () => Promise.resolve(getMockBuckets()), + getBuckets: ({region}) => + Promise.resolve(getMockBuckets().filter((b) => b.region === region)), listObjects: (input) => { if (!input) { throw new Error('need to pass input'); @@ -152,12 +167,6 @@ export const mockImplementation: ProviderSpecifics = { callFunctionAsync: getMockCallFunctionAsync, callFunctionStreaming: getMockCallFunctionStreaming, callFunctionSync: getMockCallFunctionSync, - getCurrentFunctionName: () => - speculateFunctionName({ - diskSizeInMb: 10240, - memorySizeInMb: 3009, - timeoutInSeconds: 120, - }), estimatePrice, getOutputUrl: () => { return { @@ -166,12 +175,32 @@ export const mockImplementation: ProviderSpecifics = { }; }, isFlakyError, + deleteFunction: deleteMockFunction, + getFunctions: getAllMockFunctions, }; -export const mockServerImplementation: ServerProviderSpecifics = { +export const mockServerImplementation: InsideFunctionSpecifics = { forgetBrowserEventLoop: () => {}, getBrowserInstance, timer: () => ({ end: () => {}, }), + deleteTmpDir: () => Promise.resolve(), + generateRandomId: randomHashImplementation, + getCurrentFunctionName: () => + speculateFunctionName({ + diskSizeInMb: 10240, + memorySizeInMb: 3009, + timeoutInSeconds: 120, + }), + getCurrentMemorySizeInMb: () => 3009, +}; + +export const mockFullClientSpecifics: FullClientSpecifics = { + bundleSite: mockBundleSite, + id: '__remotion_full_client_specifics', + readDirectory: mockReadDirectory, + uploadDir: mockUploadDir, + createFunction: mockCreateFunction, + checkCredentials: () => undefined, }; diff --git a/packages/lambda/src/test/mocks/aws-clients.ts b/packages/lambda/src/test/mocks/aws-clients.ts index 990bdb87dcb..51cf16c3320 100644 --- a/packages/lambda/src/test/mocks/aws-clients.ts +++ b/packages/lambda/src/test/mocks/aws-clients.ts @@ -9,7 +9,7 @@ import type { ServerlessRoutines, StreamingMessage, } from '@remotion/serverless'; -import {ResponseStream, streamWriter} from '@remotion/serverless'; +import {innerHandler, ResponseStream, streamWriter} from '@remotion/serverless'; import type {MessageTypeId} from '@remotion/serverless/client'; import { formatMap, @@ -17,6 +17,7 @@ import { } from '@remotion/serverless/client'; import {makeStreamer} from '@remotion/streaming'; import type {AwsProvider} from '../../functions/aws-implementation'; +import {getWebhookClient} from '../../functions/http-client'; import {parseJsonOrThrowSource} from '../../shared/call-lambda-streaming'; import { mockImplementation, @@ -31,8 +32,6 @@ export const getMockCallFunctionStreaming: CallFunctionStreaming< retriesRemaining: number; }, ) => { - const {innerRoutine} = await import('../../functions/index'); - const responseStream = new ResponseStream(); const {onData, clear} = makeStreamer((status, messageTypeId, data) => { @@ -55,7 +54,7 @@ export const getMockCallFunctionStreaming: CallFunctionStreaming< params.receivedStreamingPayload(message); }); - await innerRoutine({ + await innerHandler({ params: params.payload, responseWriter: { write(message) { @@ -72,9 +71,9 @@ export const getMockCallFunctionStreaming: CallFunctionStreaming< getRemainingTimeInMillis: () => params.timeoutInTest ?? 120000, awsRequestId: 'fake', }, - providerSpecifics: mockImplementation, - serverProviderSpecifics: mockServerImplementation, + insideFunctionSpecifics: mockServerImplementation, + webhookClient: getWebhookClient, }); responseStream._finish(); @@ -86,19 +85,18 @@ export const getMockCallFunctionAsync: CallFunctionAsync = async < >( params: CallFunctionOptions, ) => { - const {innerRoutine} = await import('../../functions/index'); - const responseStream = new ResponseStream(); - await innerRoutine({ + await innerHandler({ context: { invokedFunctionArn: 'arn:fake', getRemainingTimeInMillis: () => params.timeoutInTest ?? 120000, awsRequestId: 'fake', }, providerSpecifics: mockImplementation, - serverProviderSpecifics: mockServerImplementation, + insideFunctionSpecifics: mockServerImplementation, params: params.payload, responseWriter: streamWriter(responseStream), + webhookClient: getWebhookClient, }); responseStream._finish(); @@ -110,10 +108,8 @@ export const getMockCallFunctionSync: CallFunctionSync = async < >( params: CallFunctionOptions, ): Promise[T]> => { - const {innerRoutine} = await import('../../functions/index'); - const responseStream = new ResponseStream(); - await innerRoutine({ + await innerHandler({ context: { invokedFunctionArn: 'arn:fake', getRemainingTimeInMillis: () => params.timeoutInTest ?? 120000, @@ -122,7 +118,8 @@ export const getMockCallFunctionSync: CallFunctionSync = async < params: params.payload, responseWriter: streamWriter(responseStream), providerSpecifics: mockImplementation, - serverProviderSpecifics: mockServerImplementation, + insideFunctionSpecifics: mockServerImplementation, + webhookClient: getWebhookClient, }); responseStream._finish(); diff --git a/packages/lambda/src/shared/__mocks__/bundle-site.ts b/packages/lambda/src/test/mocks/mock-bundle-site.ts similarity index 93% rename from packages/lambda/src/shared/__mocks__/bundle-site.ts rename to packages/lambda/src/test/mocks/mock-bundle-site.ts index ead8a7af4d0..ef5e113729c 100644 --- a/packages/lambda/src/shared/__mocks__/bundle-site.ts +++ b/packages/lambda/src/test/mocks/mock-bundle-site.ts @@ -23,7 +23,7 @@ const convertArgumentsIntoOptions = ( return firstArg; }; -export const bundleSite: typeof bundle = (...args) => { +export const mockBundleSite: typeof bundle = (...args) => { const {entryPoint} = convertArgumentsIntoOptions(args); if (entryPoint === 'first') { return Promise.resolve('/path/to/bundle-1'); diff --git a/packages/lambda/src/test/mocks/mock-create-function.ts b/packages/lambda/src/test/mocks/mock-create-function.ts new file mode 100644 index 00000000000..6a8e91ff750 --- /dev/null +++ b/packages/lambda/src/test/mocks/mock-create-function.ts @@ -0,0 +1,23 @@ +import type {CreateFunction} from '@remotion/serverless'; +import {VERSION} from 'remotion/version'; +import type {AwsProvider} from '../../functions/aws-implementation'; +import {addFunction} from './mock-functions'; + +export const mockCreateFunction: CreateFunction = (input) => { + if (!input.alreadyCreated) { + addFunction( + { + functionName: input.functionName, + memorySizeInMb: input.memorySizeInMb, + timeoutInSeconds: input.timeoutInSeconds, + version: VERSION, + diskSizeInMb: input.ephemerealStorageInMb, + }, + input.region, + ); + } + + return Promise.resolve({ + FunctionName: input.functionName, + }); +}; diff --git a/packages/lambda/src/api/mock-functions.ts b/packages/lambda/src/test/mocks/mock-functions.ts similarity index 50% rename from packages/lambda/src/api/mock-functions.ts rename to packages/lambda/src/test/mocks/mock-functions.ts index 789803ddbf9..902717c00a2 100644 --- a/packages/lambda/src/api/mock-functions.ts +++ b/packages/lambda/src/test/mocks/mock-functions.ts @@ -1,5 +1,11 @@ -import type {AwsRegion} from '../regions'; -import type {FunctionInfo} from './get-function-info'; +import type { + DeleteFunction, + FunctionInfo, + GetFunctions, +} from '@remotion/serverless'; +import {VERSION} from 'remotion/version'; +import type {AwsProvider} from '../../functions/aws-implementation'; +import type {AwsRegion} from '../../regions'; export let mockFunctionsStore: (FunctionInfo & { region: AwsRegion; @@ -14,10 +20,14 @@ export const addFunction = (fn: FunctionInfo, region: AwsRegion) => { }); }; -export const deleteMockFunction = (name: string, region: string) => { +export const deleteMockFunction: DeleteFunction = ({ + functionName, + region, +}) => { mockFunctionsStore = mockFunctionsStore.filter( - (fn) => fn.functionName !== name && fn.region !== region, + (fn) => fn.functionName !== functionName && fn.region !== region, ); + return Promise.resolve(); }; export const findFunction = (name: string, region: string) => { @@ -26,9 +36,18 @@ export const findFunction = (name: string, region: string) => { ); }; -export const getAllMockFunctions = (region: string, version: string | null) => { - return mockFunctionsStore.filter( - (f) => f.region === region && (version ? f.version === version : true), +export const getAllMockFunctions: GetFunctions = ({ + compatibleOnly, + region, +}) => { + return Promise.resolve( + mockFunctionsStore + .filter( + (f) => + f.region === region && + (compatibleOnly ? f.version === VERSION : true), + ) + .map(({region: _region, ...f}) => f), ); }; diff --git a/packages/lambda/src/shared/__mocks__/read-dir.ts b/packages/lambda/src/test/mocks/mock-read-dir.ts similarity index 68% rename from packages/lambda/src/shared/__mocks__/read-dir.ts rename to packages/lambda/src/test/mocks/mock-read-dir.ts index f1f83deac82..225effc481d 100644 --- a/packages/lambda/src/shared/__mocks__/read-dir.ts +++ b/packages/lambda/src/test/mocks/mock-read-dir.ts @@ -1,7 +1,7 @@ -import {getDirFiles} from '../../api/__mocks__/upload-dir'; import type {readDirectory as original} from '../../shared/read-dir'; +import {getDirFiles} from './upload-dir'; -export const readDirectory: typeof original = ({dir}) => { +export const mockReadDirectory: typeof original = ({dir}) => { const files = getDirFiles(dir); const obj: Record Promise> = {}; diff --git a/packages/lambda/src/api/__mocks__/upload-dir.ts b/packages/lambda/src/test/mocks/upload-dir.ts similarity index 82% rename from packages/lambda/src/api/__mocks__/upload-dir.ts rename to packages/lambda/src/test/mocks/upload-dir.ts index 62071026d06..9581270a9d6 100644 --- a/packages/lambda/src/api/__mocks__/upload-dir.ts +++ b/packages/lambda/src/test/mocks/upload-dir.ts @@ -1,5 +1,5 @@ -import {writeMockS3File} from '../../test/mocks/mock-store'; -import type {MockFile, uploadDir as original} from '../upload-dir'; +import type {MockFile, uploadDir as original} from '../../api/upload-dir'; +import {writeMockS3File} from './mock-store'; export const getDirFiles = (dir: string): MockFile[] => { if (dir === '/path/to/bundle-1') { @@ -31,7 +31,7 @@ export const getDirFiles = (dir: string): MockFile[] => { throw new Error('could not get dir for ' + dir); }; -export const uploadDir: typeof original = (input) => { +export const mockUploadDir: typeof original = (input) => { const files = getDirFiles(input.localDir); for (const file of files) { writeMockS3File({ diff --git a/packages/lambda/src/test/setup.ts b/packages/lambda/src/test/setup.ts deleted file mode 100644 index 646812234ee..00000000000 --- a/packages/lambda/src/test/setup.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {vi} from 'vitest'; - -vi.mock('../cli/helpers/quit', () => - vi.importActual('../cli/helpers/__mocks__/quit'), -); -vi.mock('../shared/bundle-site', () => - vi.importActual('../shared/__mocks__/bundle-site'), -); -vi.mock('../shared/get-account-id', () => - vi.importActual('../shared/__mocks__/get-account-id'), -); -vi.mock('../shared/read-dir', () => - vi.importActual('../shared/__mocks__/read-dir'), -); -vi.mock('../api/upload-dir', () => - vi.importActual('../api/__mocks__/upload-dir'), -); -vi.mock('../api/clean-items', () => - vi.importActual('../api/__mocks__/clean-items'), -); -vi.mock('../api/create-function', () => - vi.importActual('../api/__mocks__/create-function'), -); -vi.mock('../api/get-functions', () => - vi.importActual('../api/__mocks__/get-functions'), -); -vi.mock('../api/delete-function', () => - vi.importActual('../api/__mocks__/delete-function'), -); -vi.mock('../shared/check-credentials', () => - vi.importActual('../shared/__mocks__/check-credentials'), -); diff --git a/packages/lambda/src/test/unit/best-bucketnames.test.ts b/packages/lambda/src/test/unit/best-bucketnames.test.ts index 7bdfb72fc66..71647781fd2 100644 --- a/packages/lambda/src/test/unit/best-bucketnames.test.ts +++ b/packages/lambda/src/test/unit/best-bucketnames.test.ts @@ -1,5 +1,5 @@ import {makeBucketName} from '@remotion/serverless/client'; -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {parseBucketName} from '../../shared/validate-bucketname'; import {mockImplementation} from '../mock-implementation'; diff --git a/packages/lambda/src/test/unit/cloudwatch-validation.test.ts b/packages/lambda/src/test/unit/cloudwatch-validation.test.ts index 16e29545d28..7ff3c7809d6 100644 --- a/packages/lambda/src/test/unit/cloudwatch-validation.test.ts +++ b/packages/lambda/src/test/unit/cloudwatch-validation.test.ts @@ -1,4 +1,4 @@ -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {validateCloudWatchRetentionPeriod} from '../../shared/validate-retention-period'; test('Should be a valid cloudwatch retention period', () => { diff --git a/packages/lambda/src/test/unit/encode-aws-url.test.ts b/packages/lambda/src/test/unit/encode-aws-url.test.ts index 8b4e4804f4f..c18a626ccf7 100644 --- a/packages/lambda/src/test/unit/encode-aws-url.test.ts +++ b/packages/lambda/src/test/unit/encode-aws-url.test.ts @@ -1,4 +1,4 @@ -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {encodeAwsUrlParams} from '../../shared/encode-aws-url-params'; test('Encode AWS URL test', () => { diff --git a/packages/lambda/src/test/unit/handlers.test.ts b/packages/lambda/src/test/unit/handlers.test.ts index 2aa6b520110..c342b55c3e9 100644 --- a/packages/lambda/src/test/unit/handlers.test.ts +++ b/packages/lambda/src/test/unit/handlers.test.ts @@ -1,5 +1,5 @@ import {ServerlessRoutines} from '@remotion/serverless/client'; -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {mockImplementation} from '../mock-implementation'; test('Info handler should return version', async () => { diff --git a/packages/lambda/src/test/unit/hosted-layers-match.test.ts b/packages/lambda/src/test/unit/hosted-layers-match.test.ts index 12c3311cf2d..cb80ac1bd87 100644 --- a/packages/lambda/src/test/unit/hosted-layers-match.test.ts +++ b/packages/lambda/src/test/unit/hosted-layers-match.test.ts @@ -1,4 +1,4 @@ -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import { REMOTION_HOSTED_LAYER_ARN, hostedLayers, diff --git a/packages/lambda/src/test/unit/make-s3-key.test.ts b/packages/lambda/src/test/unit/make-s3-key.test.ts index e64ba30acf5..f7799b6c2d0 100644 --- a/packages/lambda/src/test/unit/make-s3-key.test.ts +++ b/packages/lambda/src/test/unit/make-s3-key.test.ts @@ -1,4 +1,4 @@ -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {makeS3Key} from '../../shared/make-s3-key'; test('makeS3Key should return POSIX syntax cross-platform', () => { diff --git a/packages/lambda/src/test/unit/price-calculation.test.ts b/packages/lambda/src/test/unit/price-calculation.test.ts index 635969bb2c4..fa7d65d4f9f 100644 --- a/packages/lambda/src/test/unit/price-calculation.test.ts +++ b/packages/lambda/src/test/unit/price-calculation.test.ts @@ -1,10 +1,9 @@ import {estimatePriceFromBucket} from '@remotion/serverless'; -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {awsImplementation} from '../../functions/aws-implementation'; test('Should not throw while calculating prices when time shifts occur', () => { const aDate = Date.now(); - process.env.__RESERVED_IS_INSIDE_REMOTION_LAMBDA = 'true'; process.env.AWS_REGION = 'us-east-1'; const price = estimatePriceFromBucket({ @@ -46,7 +45,7 @@ test('Should not throw while calculating prices when time shifts occur', () => { }, }, diskSizeInMb: 512, - lambdasInvoked: 1, + functionsInvoked: 1, timings: [ { chunk: 1, diff --git a/packages/lambda/src/test/unit/pricing.test.ts b/packages/lambda/src/test/unit/pricing.test.ts index 0f049169cf7..ca3377802ee 100644 --- a/packages/lambda/src/test/unit/pricing.test.ts +++ b/packages/lambda/src/test/unit/pricing.test.ts @@ -1,4 +1,4 @@ -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {estimatePrice} from '../../api/estimate-price'; test('Should calculate costs accurately', () => { diff --git a/packages/lambda/src/test/unit/validate-disk-size-in-mb.test.ts b/packages/lambda/src/test/unit/validate-disk-size-in-mb.test.ts index fd52eb14f62..8bd9cefa376 100644 --- a/packages/lambda/src/test/unit/validate-disk-size-in-mb.test.ts +++ b/packages/lambda/src/test/unit/validate-disk-size-in-mb.test.ts @@ -1,4 +1,4 @@ -import {expect, test} from 'vitest'; +import {expect, test} from 'bun:test'; import {validateDiskSizeInMb} from '../../shared/validate-disk-size-in-mb'; test('Disk size tests', () => { diff --git a/packages/lambda/vitest.config.mts b/packages/lambda/vitest.config.mts deleted file mode 100644 index 1727d3f97f7..00000000000 --- a/packages/lambda/vitest.config.mts +++ /dev/null @@ -1,11 +0,0 @@ -import {defineConfig} from 'vitest/config'; - -export default defineConfig({ - test: { - setupFiles: ['./src/test/setup.ts'], - maxConcurrency: 1, - testTimeout: 90000, - maxThreads: 1, - minThreads: 1, - }, -}); diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index 36c4066dacd..b2b28a08fbc 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -95,7 +95,7 @@ export {CancelSignal, makeCancelSignal} from './make-cancel-signal'; export {openBrowser} from './open-browser'; export type {ChromiumOptions} from './open-browser'; export {ColorSpace} from './options/color-space'; -export {DeleteAfter} from './options/delete-after'; +export type {DeleteAfter} from './options/delete-after'; export {OpenGlRenderer} from './options/gl'; export {NumberOfGifLoops} from './options/number-of-gif-loops'; export { diff --git a/packages/renderer/src/serve-static.ts b/packages/renderer/src/serve-static.ts index 7dbaf380a9b..bbe5a26f8bc 100644 --- a/packages/renderer/src/serve-static.ts +++ b/packages/renderer/src/serve-static.ts @@ -64,7 +64,14 @@ export const serveStatic = async ( }); server.on('connection', (conn) => { - const key = conn.remoteAddress + ':' + conn.remotePort; + let key; + // Bun 1.0.43 fails on this + try { + key = conn.remoteAddress + ':' + conn.remotePort; + } catch { + key = ':'; + } + connections[key] = conn; conn.on('close', () => { delete connections[key]; diff --git a/packages/serverless/package.json b/packages/serverless/package.json index 23551633979..4dfb6a0bf8a 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -25,7 +25,8 @@ "dependencies": { "remotion": "workspace:*", "@remotion/renderer": "workspace:*", - "@remotion/streaming": "workspace:*" + "@remotion/streaming": "workspace:*", + "@remotion/bundler": "workspace:*" }, "devDependencies": { "@remotion/eslint-config-internal": "workspace:*", diff --git a/packages/serverless/src/best-frames-per-function-param.ts b/packages/serverless/src/best-frames-per-function-param.ts index 412c9850308..93982fecea0 100644 --- a/packages/serverless/src/best-frames-per-function-param.ts +++ b/packages/serverless/src/best-frames-per-function-param.ts @@ -8,11 +8,11 @@ export const bestFramesPerFunctionParam = (frameCount: number) => { extrapolateRight: 'clamp', }); - // At least have 20 as a `framesPerLambda` value - const framesPerLambda = Math.max(frameCount / concurrency, 20); + // At least have 20 as a `framesPerFunction` value + const framesPerFunction = Math.max(frameCount / concurrency, 20); - // Evenly distribute: For 21 frames over 2 lambda functions, distribute as 11 + 10 ==> framesPerLambda = 11 - const lambdasNeeded = Math.ceil(frameCount / framesPerLambda); + // Evenly distribute: For 21 frames over 2 functions, distribute as 11 + 10 ==> framesPerLambda = 11 + const functionsNeeded = Math.ceil(frameCount / framesPerFunction); - return Math.ceil(frameCount / lambdasNeeded); + return Math.ceil(frameCount / functionsNeeded); }; diff --git a/packages/serverless/src/client.ts b/packages/serverless/src/client.ts index 620609b5cf2..3b77aa02bff 100644 --- a/packages/serverless/src/client.ts +++ b/packages/serverless/src/client.ts @@ -1,3 +1,5 @@ +export {COMMAND_NOT_FOUND} from './constants'; + export {Await} from './await'; export { compressInputProps, diff --git a/packages/serverless/src/compress-props.ts b/packages/serverless/src/compress-props.ts index 0e70def1687..fe665d61902 100644 --- a/packages/serverless/src/compress-props.ts +++ b/packages/serverless/src/compress-props.ts @@ -35,13 +35,20 @@ export const serializeOrThrow = ( } }; -export const getNeedsToUpload = ( - type: 'still' | 'video-or-audio', - sizes: number[], -) => { +export const getNeedsToUpload = ({ + type, + sizes, + providerSpecifics, +}: { + type: 'still' | 'video-or-audio'; + sizes: number[]; + providerSpecifics: ProviderSpecifics; +}) => { const MARGIN = 5_000 + MAX_WEBHOOK_CUSTOM_DATA_SIZE; const MAX_INLINE_PAYLOAD_SIZE = - (type === 'still' ? 5_000_000 : 200_000) - MARGIN; + (type === 'still' + ? providerSpecifics.getMaxStillInlinePayloadSize() + : providerSpecifics.getMaxNonInlinePayloadSizePerFunction()) - MARGIN; const sizesAlreadyUsed = sizes.reduce((a, b) => a + b); @@ -52,7 +59,7 @@ export const getNeedsToUpload = ( MAX_INLINE_PAYLOAD_SIZE / 1000, )}KB (${Math.ceil( sizesAlreadyUsed / 1024, - )}KB) in size. Uploading them to S3 to circumvent AWS Lambda payload size, which may lead to slowdown.`, + )}KB) in size. Uploading them to ${providerSpecifics.serverStorageProductName()} to circumvent AWS Lambda payload size, which may lead to slowdown.`, ); return true; } diff --git a/packages/serverless/src/concat-videos.ts b/packages/serverless/src/concat-videos.ts index 7c1e9648ec3..e7b1ea8dcf6 100644 --- a/packages/serverless/src/concat-videos.ts +++ b/packages/serverless/src/concat-videos.ts @@ -12,7 +12,7 @@ import { REMOTION_FILELIST_TOKEN, type ServerlessCodec, } from './constants'; -import type {ServerProviderSpecifics} from './provider-implementation'; +import type {InsideFunctionSpecifics} from './provider-implementation'; export const concatVideos = async ({ onProgress, @@ -31,7 +31,7 @@ export const concatVideos = async ({ preferLossless, muted, metadata, - serverProviderSpecifics, + insideFunctionSpecifics, }: { onProgress: (frames: number) => void; numberOfFrames: number; @@ -49,13 +49,13 @@ export const concatVideos = async ({ preferLossless: boolean; muted: boolean; metadata: Record | null; - serverProviderSpecifics: ServerProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; }) => { const outfile = join( RenderInternals.tmpDir(REMOTION_CONCATED_TOKEN), `concat.${RenderInternals.getFileExtensionFromCodec(codec, audioCodec)}`, ); - const combine = serverProviderSpecifics.timer('Combine chunks', logLevel); + const combine = insideFunctionSpecifics.timer('Combine chunks', logLevel); const filelistDir = RenderInternals.tmpDir(REMOTION_FILELIST_TOKEN); const chunkDurationInSeconds = framesPerLambda / fps; diff --git a/packages/serverless/src/constants.ts b/packages/serverless/src/constants.ts index b4146908ca8..4597fa00ec6 100644 --- a/packages/serverless/src/constants.ts +++ b/packages/serverless/src/constants.ts @@ -14,7 +14,7 @@ import type { import type {BrowserSafeApis} from '@remotion/renderer/client'; import type {ExpensiveChunk} from './most-expensive-chunks'; import type {ChunkRetry, CloudProvider, ReceivedArtifact} from './types'; -import type {EnhancedErrorInfo} from './write-lambda-error'; +import type {EnhancedErrorInfo} from './write-error-to-storage'; // Needs to be in sync with renderer/src/options/delete-after.ts#L7 export const expiryDays = { @@ -174,7 +174,7 @@ export type ServerlessPayloads = { type: ServerlessRoutines.launch; serveUrl: string; composition: string; - framesPerLambda: number | null; + framesPerFunction: number | null; bucketName: string; inputProps: SerializedInputProps; renderId: string; @@ -197,7 +197,7 @@ export type ServerlessPayloads = { scale: number; everyNthFrame: number; numberOfGifLoops: number | null; - concurrencyPerLambda: number; + concurrencyPerFunction: number; downloadBehavior: DownloadBehavior; muted: boolean; overwrite: boolean; @@ -397,3 +397,4 @@ export const REMOTION_CONCATED_TOKEN = 'remotion-concated-token'; export const REMOTION_FILELIST_TOKEN = 'remotion-filelist'; export const RENDERER_PATH_TOKEN = 'remotion-bucket'; +export const COMMAND_NOT_FOUND = 'Command not found'; diff --git a/packages/serverless/src/create-post-render-data.ts b/packages/serverless/src/create-post-render-data.ts index 43f8b270e64..8bf2aed7b1c 100644 --- a/packages/serverless/src/create-post-render-data.ts +++ b/packages/serverless/src/create-post-render-data.ts @@ -9,7 +9,7 @@ import type {OverallRenderProgress} from './overall-render-progress'; import type {ProviderSpecifics} from './provider-implementation'; import type {RenderMetadata} from './render-metadata'; import type {CloudProvider} from './types'; -import type {EnhancedErrorInfo} from './write-lambda-error'; +import type {EnhancedErrorInfo} from './write-error-to-storage'; export const createPostRenderData = ({ region, @@ -68,7 +68,7 @@ export const createPostRenderData = ({ cost: { currency: 'USD', disclaimer: - 'Estimated cost for lambda invocations only. Does not include cost for S3 storage and data transfer.', + 'Estimated cost for function invocations only. Does not include cost for storage and data transfer.', estimatedCost: cost, estimatedDisplayCost: `$${new Intl.NumberFormat('en-US', { currency: 'USD', @@ -96,7 +96,7 @@ export const createPostRenderData = ({ ? [] : getMostExpensiveChunks({ parsedTimings, - framesPerLambda: renderMetadata.framesPerLambda, + framesPerFunction: renderMetadata.framesPerLambda, firstFrame: renderMetadata.frameRange[0], lastFrame: renderMetadata.frameRange[1], }), diff --git a/packages/serverless/src/estimate-price-from-bucket.ts b/packages/serverless/src/estimate-price-from-bucket.ts index ad9d6066feb..6ad3ef64789 100644 --- a/packages/serverless/src/estimate-price-from-bucket.ts +++ b/packages/serverless/src/estimate-price-from-bucket.ts @@ -3,11 +3,11 @@ import type {ProviderSpecifics} from './provider-implementation'; import type {RenderMetadata} from './render-metadata'; import type {CloudProvider, ParsedTiming} from './types'; -export const estimatePriceFromBucket = ({ +export const estimatePriceFromMetadata = ({ renderMetadata, memorySizeInMb, diskSizeInMb, - lambdasInvoked, + functionsInvoked, timings, region, providerSpecifics, @@ -15,7 +15,7 @@ export const estimatePriceFromBucket = ({ renderMetadata: RenderMetadata | null; memorySizeInMb: number; diskSizeInMb: number; - lambdasInvoked: number; + functionsInvoked: number; timings: ParsedTiming[]; region: Provider['region']; providerSpecifics: ProviderSpecifics; @@ -50,7 +50,7 @@ export const estimatePriceFromBucket = ({ durationInMilliseconds: estimatedBillingDurationInMilliseconds, memorySizeInMb, diskSizeInMb, - lambdasInvoked, + lambdasInvoked: functionsInvoked, }) .toPrecision(5), ); diff --git a/packages/serverless/src/get-browser-instance.ts b/packages/serverless/src/get-browser-instance.ts index 5ed288dd18a..0d36cf2f1e1 100644 --- a/packages/serverless/src/get-browser-instance.ts +++ b/packages/serverless/src/get-browser-instance.ts @@ -4,8 +4,8 @@ import {VERSION} from 'remotion/version'; import type {Await} from './await'; import type { GetBrowserInstance, + InsideFunctionSpecifics, ProviderSpecifics, - ServerProviderSpecifics, } from './provider-implementation'; import type {CloudProvider} from './types'; @@ -65,13 +65,13 @@ export const getBrowserInstanceImplementation: GetBrowserInstance = async < indent, chromiumOptions, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }: { logLevel: LogLevel; indent: boolean; chromiumOptions: ChromiumOptions; providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; }): Promise => { const actualChromiumOptions: ChromiumOptions = { ...chromiumOptions, @@ -125,7 +125,7 @@ export const getBrowserInstanceImplementation: GetBrowserInstance = async < {indent: false, logLevel}, 'Browser disconnected or crashed.', ); - serverProviderSpecifics.forgetBrowserEventLoop(logLevel); + insideFunctionSpecifics.forgetBrowserEventLoop(logLevel); _browserInstance?.instance?.close(true, logLevel, indent).catch((err) => { RenderInternals.Log.info( {indent: false, logLevel}, @@ -152,12 +152,12 @@ export const getBrowserInstanceImplementation: GetBrowserInstance = async < _browserInstance.instance.runner.rememberEventLoop(); await _browserInstance.instance.close(true, logLevel, indent); _browserInstance = null; - return serverProviderSpecifics.getBrowserInstance({ + return insideFunctionSpecifics.getBrowserInstance({ logLevel, indent, chromiumOptions, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }); } diff --git a/packages/serverless/src/handlers/check-version-mismatch.ts b/packages/serverless/src/handlers/check-version-mismatch.ts new file mode 100644 index 00000000000..22d7f8c9e70 --- /dev/null +++ b/packages/serverless/src/handlers/check-version-mismatch.ts @@ -0,0 +1,38 @@ +import {VERSION} from 'remotion/version'; +import {ServerlessRoutines, type ServerlessPayload} from '../constants'; +import type {InsideFunctionSpecifics} from '../provider-implementation'; +import type {CloudProvider} from '../types'; + +export const checkVersionMismatch = ({ + params, + insideFunctionSpecifics, + apiName, +}: { + params: ServerlessPayload; + insideFunctionSpecifics: InsideFunctionSpecifics; + apiName: string; +}) => { + if (params.type === ServerlessRoutines.info) { + return; + } + + if (params.type === ServerlessRoutines.renderer) { + return; + } + + if (params.type === ServerlessRoutines.launch) { + return; + } + + if (params.version !== VERSION) { + if (!params.version) { + throw new Error( + `Version mismatch: When calling ${apiName}, you called the function ${insideFunctionSpecifics.getCurrentFunctionName()} which has the version ${VERSION} but the @remotion/lambda package is an older version. Deploy a new function and use it to call ${apiName}. See: https://www.remotion.dev/docs/lambda/upgrading`, + ); + } + + throw new Error( + `Version mismatch: When calling ${apiName}, you passed ${insideFunctionSpecifics.getCurrentFunctionName()} as the function, which has the version ${VERSION}, but the @remotion/lambda package you used to invoke the function has version ${params.version}. Deploy a new function and use it to call getRenderProgress(). See: https://www.remotion.dev/docs/lambda/upgrading`, + ); + } +}; diff --git a/packages/serverless/src/handlers/compositions.ts b/packages/serverless/src/handlers/compositions.ts index 6fd5eead52d..49c0edee72f 100644 --- a/packages/serverless/src/handlers/compositions.ts +++ b/packages/serverless/src/handlers/compositions.ts @@ -1,60 +1,59 @@ import {RenderInternals} from '@remotion/renderer'; -import {VERSION} from 'remotion/version'; import {decompressInputProps} from '../compress-props'; import type {ServerlessPayload} from '../constants'; import {ServerlessRoutines} from '../constants'; import {} from '../get-browser-instance'; import {internalGetOrCreateBucket} from '../get-or-create-bucket'; import type { + InsideFunctionSpecifics, ProviderSpecifics, - ServerProviderSpecifics, } from '../provider-implementation'; import type {CloudProvider} from '../types'; +import {checkVersionMismatch} from './check-version-mismatch'; type Options = { expectedBucketOwner: string; }; -export const compositionsHandler = async ( - lambdaParams: ServerlessPayload, - options: Options, - providerSpecifics: ProviderSpecifics, - serverProviderSpecifics: ServerProviderSpecifics, -) => { - if (lambdaParams.type !== ServerlessRoutines.compositions) { +export const compositionsHandler = async ({ + params, + options, + providerSpecifics, + insideFunctionSpecifics, +}: { + params: ServerlessPayload; + options: Options; + providerSpecifics: ProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; +}) => { + if (params.type !== ServerlessRoutines.compositions) { throw new TypeError('Expected info compositions'); } - if (lambdaParams.version !== VERSION) { - if (!lambdaParams.version) { - throw new Error( - `Version mismatch: When calling getCompositionsOnLambda(), you called the function ${process.env.AWS_LAMBDA_FUNCTION_NAME} which has the version ${VERSION} but the @remotion/lambda package is an older version. Deploy a new function and use it to call getCompositionsOnLambda(). See: https://www.remotion.dev/docs/lambda/upgrading`, - ); - } - - throw new Error( - `Version mismatch: When calling getCompositionsOnLambda(), you passed ${process.env.AWS_LAMBDA_FUNCTION_NAME} as the function, which has the version ${VERSION}, but the @remotion/lambda package you used to invoke the function has version ${lambdaParams.version}. Deploy a new function and use it to call getCompositionsOnLambda(). See: https://www.remotion.dev/docs/lambda/upgrading`, - ); - } + checkVersionMismatch({ + apiName: 'getCompositionsOnLambda()', + insideFunctionSpecifics, + params, + }); try { const region = providerSpecifics.getCurrentRegionInFunction(); - const browserInstancePromise = serverProviderSpecifics.getBrowserInstance({ - logLevel: lambdaParams.logLevel, + const browserInstancePromise = insideFunctionSpecifics.getBrowserInstance({ + logLevel: params.logLevel, indent: false, - chromiumOptions: lambdaParams.chromiumOptions, + chromiumOptions: params.chromiumOptions, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }); - const bucketNamePromise = lambdaParams.bucketName - ? Promise.resolve(lambdaParams.bucketName) + const bucketNamePromise = params.bucketName + ? Promise.resolve(params.bucketName) : internalGetOrCreateBucket({ region, enableFolderExpiry: null, customCredentials: null, providerSpecifics, - forcePathStyle: lambdaParams.forcePathStyle, + forcePathStyle: params.forcePathStyle, skipPutAcl: false, }).then((b) => b.bucketName); @@ -63,14 +62,14 @@ export const compositionsHandler = async ( bucketName: await bucketNamePromise, expectedBucketOwner: options.expectedBucketOwner, region: providerSpecifics.getCurrentRegionInFunction(), - serialized: lambdaParams.inputProps, + serialized: params.inputProps, propsType: 'input-props', providerSpecifics, - forcePathStyle: lambdaParams.forcePathStyle, + forcePathStyle: params.forcePathStyle, }); const realServeUrl = providerSpecifics.convertToServeUrl({ - urlOrId: lambdaParams.serveUrl, + urlOrId: params.serveUrl, region, bucketName, }); @@ -79,20 +78,19 @@ export const compositionsHandler = async ( serveUrlOrWebpackUrl: realServeUrl, puppeteerInstance: (await browserInstancePromise).instance, serializedInputPropsWithCustomSchema, - envVariables: lambdaParams.envVariables ?? {}, - timeoutInMilliseconds: lambdaParams.timeoutInMilliseconds, - chromiumOptions: lambdaParams.chromiumOptions, + envVariables: params.envVariables ?? {}, + timeoutInMilliseconds: params.timeoutInMilliseconds, + chromiumOptions: params.chromiumOptions, port: null, server: undefined, - logLevel: lambdaParams.logLevel, + logLevel: params.logLevel, indent: false, browserExecutable: null, onBrowserLog: null, - offthreadVideoCacheSizeInBytes: - lambdaParams.offthreadVideoCacheSizeInBytes, + offthreadVideoCacheSizeInBytes: params.offthreadVideoCacheSizeInBytes, binariesDirectory: null, onBrowserDownload: () => { - throw new Error('Should not download a browser in Lambda'); + throw new Error('Should not download a browser in a function'); }, }); @@ -101,6 +99,6 @@ export const compositionsHandler = async ( type: 'success' as const, }); } finally { - serverProviderSpecifics.forgetBrowserEventLoop(lambdaParams.logLevel); + insideFunctionSpecifics.forgetBrowserEventLoop(params.logLevel); } }; diff --git a/packages/serverless/src/handlers/launch.ts b/packages/serverless/src/handlers/launch.ts index 7ae4c86e836..ddaef026e77 100644 --- a/packages/serverless/src/handlers/launch.ts +++ b/packages/serverless/src/handlers/launch.ts @@ -26,8 +26,8 @@ import {invokeWebhook} from '../invoke-webhook'; import type {OverallProgressHelper} from '../overall-render-progress'; import {makeOverallRenderProgress} from '../overall-render-progress'; import type { + InsideFunctionSpecifics, ProviderSpecifics, - ServerProviderSpecifics, } from '../provider-implementation'; import type {RenderMetadata} from '../render-metadata'; @@ -38,16 +38,11 @@ import {mergeChunksAndFinishRender} from '../merge-chunks'; import {planFrameRanges} from '../plan-frame-ranges'; import {streamRendererFunctionWithRetry} from '../stream-renderer'; import type {CloudProvider} from '../types'; -import { - validateDimension, - validateDurationInFrames, - validateFps, -} from '../validate'; import {validateComposition} from '../validate-composition'; import {validateFramesPerFunction} from '../validate-frames-per-function'; import {validateOutname} from '../validate-outname'; import {validatePrivacy} from '../validate-privacy'; -import {getTmpDirStateIfENoSp} from '../write-lambda-error'; +import {getTmpDirStateIfENoSp} from '../write-error-to-storage'; type Options = { expectedBucketOwner: string; @@ -61,7 +56,7 @@ const innerLaunchHandler = async ({ overallProgress, registerCleanupTask, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }: { functionName: string; params: ServerlessPayload; @@ -69,7 +64,7 @@ const innerLaunchHandler = async ({ overallProgress: OverallProgressHelper; registerCleanupTask: (cleanupTask: CleanupTask) => void; providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; }): Promise> => { if (params.type !== ServerlessRoutines.launch) { throw new Error('Expected launch type'); @@ -77,12 +72,12 @@ const innerLaunchHandler = async ({ const startedDate = Date.now(); - const browserInstance = serverProviderSpecifics.getBrowserInstance({ + const browserInstance = insideFunctionSpecifics.getBrowserInstance({ logLevel: params.logLevel, indent: false, chromiumOptions: params.chromiumOptions, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }); const inputPropsPromise = decompressInputProps({ @@ -128,7 +123,7 @@ const innerLaunchHandler = async ({ server: undefined, offthreadVideoCacheSizeInBytes: params.offthreadVideoCacheSizeInBytes, onBrowserDownload: () => { - throw new Error('Should not download a browser in Lambda'); + throw new Error('Should not download a browser in a function'); }, onServeUrlVisited: () => { overallProgress.setServeUrlOpened(Date.now()); @@ -142,19 +137,11 @@ const innerLaunchHandler = async ({ comp.props, ); - validateDurationInFrames(comp.durationInFrames, { - component: 'passed to a Lambda render', - allowFloats: false, - }); - validateFps(comp.fps, 'passed to a Lambda render', false); - validateDimension(comp.height, 'height', 'passed to a Lambda render'); - validateDimension(comp.width, 'width', 'passed to a Lambda render'); - RenderInternals.validateBitrate(params.audioBitrate, 'audioBitrate'); RenderInternals.validateBitrate(params.videoBitrate, 'videoBitrate'); RenderInternals.validateConcurrency({ - value: params.concurrencyPerLambda, + value: params.concurrencyPerFunction, setting: 'concurrencyPerLambda', checkIfValidForCurrentMachine: (params.rendererFunctionName ?? null) === null, @@ -171,10 +158,10 @@ const innerLaunchHandler = async ({ ); const framesPerLambda = - params.framesPerLambda ?? bestFramesPerFunctionParam(frameCount.length); + params.framesPerFunction ?? bestFramesPerFunctionParam(frameCount.length); validateFramesPerFunction({ - framesPerLambda, + framesPerFunction: framesPerLambda, durationInFrames: frameCount.length, }); @@ -188,7 +175,7 @@ const innerLaunchHandler = async ({ RenderInternals.validatePuppeteerTimeout(params.timeoutInMilliseconds); const {chunks} = planFrameRanges({ - framesPerLambda, + framesPerFunction: framesPerLambda, frameRange: realFrameRange, everyNthFrame: params.everyNthFrame, }); @@ -205,13 +192,17 @@ const innerLaunchHandler = async ({ const serializedResolved = serializeOrThrow(comp.props, 'resolved-props'); - const needsToUpload = getNeedsToUpload('video-or-audio', [ - serializedResolved.length, - params.inputProps.type === 'bucket-url' - ? params.inputProps.hash.length - : params.inputProps.payload.length, - JSON.stringify(params.envVariables).length, - ]); + const needsToUpload = getNeedsToUpload({ + type: 'video-or-audio', + sizes: [ + serializedResolved.length, + params.inputProps.type === 'bucket-url' + ? params.inputProps.hash.length + : params.inputProps.payload.length, + JSON.stringify(params.envVariables).length, + ], + providerSpecifics, + }); const serializedResolvedProps = await compressInputProps({ propsType: 'resolved-props', @@ -269,7 +260,7 @@ const innerLaunchHandler = async ({ chromiumOptions: params.chromiumOptions, scale: params.scale, everyNthFrame: params.everyNthFrame, - concurrencyPerLambda: params.concurrencyPerLambda, + concurrencyPerLambda: params.concurrencyPerFunction, muted: params.muted, audioBitrate: params.audioBitrate, videoBitrate: params.videoBitrate, @@ -316,7 +307,7 @@ const innerLaunchHandler = async ({ inputProps: params.inputProps, lambdaVersion: VERSION, framesPerLambda, - memorySizeInMb: Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE), + memorySizeInMb: insideFunctionSpecifics.getCurrentMemorySizeInMb(), region: providerSpecifics.getCurrentRegionInFunction(), renderId: params.renderId, outName: params.outName ?? undefined, @@ -330,7 +321,7 @@ const innerLaunchHandler = async ({ audioBitrate: params.audioBitrate, muted: params.muted, metadata: params.metadata, - functionName: process.env.AWS_LAMBDA_FUNCTION_NAME as string, + functionName: insideFunctionSpecifics.getCurrentFunctionName(), dimensions: { width: comp.width * (params.scale ?? 1), height: comp.height * (params.scale ?? 1), @@ -346,7 +337,7 @@ const innerLaunchHandler = async ({ ); if (!params.overwrite) { - const findOutputFile = serverProviderSpecifics.timer( + const findOutputFile = insideFunctionSpecifics.timer( 'Checking if output file already exists', params.logLevel, ); @@ -494,7 +485,7 @@ const innerLaunchHandler = async ({ startTime, providerSpecifics, forcePathStyle: params.forcePathStyle, - serverProviderSpecifics, + insideFunctionSpecifics, }); return postRenderData; @@ -507,12 +498,12 @@ export const launchHandler = async ({ options, providerSpecifics, client, - serverProviderSpecifics, + insideFunctionSpecifics, }: { params: ServerlessPayload; options: Options; providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; client: WebhookClient; }): Promise<{ type: 'success'; @@ -523,7 +514,7 @@ export const launchHandler = async ({ const functionName = params.rendererFunctionName ?? - (process.env.AWS_LAMBDA_FUNCTION_NAME as string); + insideFunctionSpecifics.getCurrentFunctionName(); const logOptions: LogOptions = { indent: false, @@ -684,7 +675,7 @@ export const launchHandler = async ({ overallProgress, registerCleanupTask, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }); clearTimeout(webhookDueToTimeout); @@ -845,6 +836,6 @@ export const launchHandler = async ({ throw err; } finally { - serverProviderSpecifics.forgetBrowserEventLoop(params.logLevel); + insideFunctionSpecifics.forgetBrowserEventLoop(params.logLevel); } }; diff --git a/packages/serverless/src/handlers/progress.ts b/packages/serverless/src/handlers/progress.ts index 563ddcb3ba2..554a1496048 100644 --- a/packages/serverless/src/handlers/progress.ts +++ b/packages/serverless/src/handlers/progress.ts @@ -1,50 +1,52 @@ -import {VERSION} from 'remotion/version'; import type {ServerlessPayload} from '../constants'; import {ServerlessRoutines} from '../constants'; import {getProgress} from '../progress'; -import type {ProviderSpecifics} from '../provider-implementation'; +import type { + InsideFunctionSpecifics, + ProviderSpecifics, +} from '../provider-implementation'; import type {GenericRenderProgress} from '../render-progress'; import type {CloudProvider} from '../types'; +import {checkVersionMismatch} from './check-version-mismatch'; type Options = { expectedBucketOwner: string; timeoutInMilliseconds: number; retriesRemaining: number; providerSpecifics: ProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; }; -export const progressHandler = async ( - lambdaParams: ServerlessPayload, - options: Options, -): Promise> => { - if (lambdaParams.type !== ServerlessRoutines.status) { +export const progressHandler = async ({ + params, + options, +}: { + params: ServerlessPayload; + options: Options; +}): Promise> => { + if (params.type !== ServerlessRoutines.status) { throw new TypeError('Expected status type'); } - if (lambdaParams.version !== VERSION) { - if (!lambdaParams.version) { - throw new Error( - `Version mismatch: When calling getRenderProgress(), you called the function ${process.env.AWS_LAMBDA_FUNCTION_NAME} which has the version ${VERSION} but the @remotion/lambda package is an older version. Deploy a new function and use it to call getRenderProgress(). See: https://www.remotion.dev/docs/lambda/upgrading`, - ); - } - - throw new Error( - `Version mismatch: When calling getRenderProgress(), you passed ${process.env.AWS_LAMBDA_FUNCTION_NAME} as the function, which has the version ${VERSION}, but the @remotion/lambda package you used to invoke the function has version ${lambdaParams.version}. Deploy a new function and use it to call getRenderProgress(). See: https://www.remotion.dev/docs/lambda/upgrading`, - ); - } + checkVersionMismatch({ + apiName: 'getRenderProgress()', + insideFunctionSpecifics: options.insideFunctionSpecifics, + params, + }); try { const progress = await getProgress({ - bucketName: lambdaParams.bucketName, - renderId: lambdaParams.renderId, + bucketName: params.bucketName, + renderId: params.renderId, expectedBucketOwner: options.expectedBucketOwner, region: options.providerSpecifics.getCurrentRegionInFunction(), - memorySizeInMb: Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE), + memorySizeInMb: + options.insideFunctionSpecifics.getCurrentMemorySizeInMb(), timeoutInMilliseconds: options.timeoutInMilliseconds, - customCredentials: lambdaParams.s3OutputProvider ?? null, + customCredentials: params.s3OutputProvider ?? null, providerSpecifics: options.providerSpecifics, - forcePathStyle: lambdaParams.forcePathStyle, - functionName: process.env.AWS_LAMBDA_FUNCTION_NAME as string, + forcePathStyle: params.forcePathStyle, + functionName: options.insideFunctionSpecifics.getCurrentFunctionName(), }); return progress; } catch (err) { @@ -56,11 +58,15 @@ export const progressHandler = async ( await new Promise((resolve) => { setTimeout(resolve, 1000); }); - return progressHandler(lambdaParams, { - expectedBucketOwner: options.expectedBucketOwner, - timeoutInMilliseconds: options.timeoutInMilliseconds, - retriesRemaining: options.retriesRemaining - 1, - providerSpecifics: options.providerSpecifics, + return progressHandler({ + params, + options: { + expectedBucketOwner: options.expectedBucketOwner, + timeoutInMilliseconds: options.timeoutInMilliseconds, + retriesRemaining: options.retriesRemaining - 1, + providerSpecifics: options.providerSpecifics, + insideFunctionSpecifics: options.insideFunctionSpecifics, + }, }); } diff --git a/packages/serverless/src/handlers/renderer.ts b/packages/serverless/src/handlers/renderer.ts index 792a7a398e7..a6a912dc56e 100644 --- a/packages/serverless/src/handlers/renderer.ts +++ b/packages/serverless/src/handlers/renderer.ts @@ -20,15 +20,15 @@ import {RENDERER_PATH_TOKEN, ServerlessRoutines} from '../constants'; import {startLeakDetection} from '../leak-detection'; import {onDownloadsHelper} from '../on-downloads-helpers'; import type { + InsideFunctionSpecifics, ProviderSpecifics, - ServerProviderSpecifics, } from '../provider-implementation'; import {serializeArtifact} from '../serialize-artifact'; import type {OnStream} from '../streaming/streaming'; import {truthy} from '../truthy'; import type {CloudProvider, ObjectChunkTimingData} from '../types'; import {enableNodeIntrospection} from '../why-is-node-running'; -import {getTmpDirStateIfENoSp} from '../write-lambda-error'; +import {getTmpDirStateIfENoSp} from '../write-error-to-storage'; type Options = { expectedBucketOwner: string; @@ -47,14 +47,14 @@ const renderHandler = async ({ logs, onStream, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }: { params: ServerlessPayload; options: Options; logs: BrowserLog[]; onStream: OnStream; providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; }): Promise<{}> => { if (params.type !== ServerlessRoutines.renderer) { throw new Error('Params must be renderer'); @@ -86,12 +86,12 @@ const renderHandler = async ({ forcePathStyle: params.forcePathStyle, }); - const browserInstance = await serverProviderSpecifics.getBrowserInstance({ + const browserInstance = await insideFunctionSpecifics.getBrowserInstance({ logLevel: params.logLevel, indent: false, chromiumOptions: params.chromiumOptions, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }); const outputPath = RenderInternals.tmpDir('remotion-render-'); @@ -343,13 +343,13 @@ const renderHandler = async ({ .catch((err) => reject(err)); }); - const streamTimer = serverProviderSpecifics.timer( + const streamTimer = insideFunctionSpecifics.timer( 'Streaming chunk to the main function', params.logLevel, ); if (audioOutputLocation) { - const audioChunkTimer = serverProviderSpecifics.timer( + const audioChunkTimer = insideFunctionSpecifics.timer( 'Sending audio chunk', params.logLevel, ); @@ -361,7 +361,7 @@ const renderHandler = async ({ } if (videoOutputLocation) { - const videoChunkTimer = serverProviderSpecifics.timer( + const videoChunkTimer = insideFunctionSpecifics.timer( 'Sending main chunk', params.logLevel, ); @@ -416,14 +416,14 @@ export const rendererHandler = async ({ params, providerSpecifics, requestContext, - serverProviderSpecifics, + insideFunctionSpecifics, }: { params: ServerlessPayload; options: Options; onStream: OnStream; requestContext: RequestContext; providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; }): Promise<{ type: 'success'; }> => { @@ -443,7 +443,7 @@ export const rendererHandler = async ({ logs, onStream, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }); return { type: 'success', @@ -503,15 +503,15 @@ export const rendererHandler = async ({ throw err; } finally { if (shouldKeepBrowserOpen) { - serverProviderSpecifics.forgetBrowserEventLoop(params.logLevel); + insideFunctionSpecifics.forgetBrowserEventLoop(params.logLevel); } else { RenderInternals.Log.info( {indent: false, logLevel: params.logLevel}, - 'Lambda did not succeed with flaky error, not keeping browser open.', + 'Function did not succeed with flaky error, not keeping browser open.', ); RenderInternals.Log.info( {indent: false, logLevel: params.logLevel}, - 'Quitting Lambda forcefully now to force not keeping the Lambda warm.', + 'Quitting Function forcefully now to force not keeping the Function warm.', ); process.exit(0); } diff --git a/packages/serverless/src/handlers/start.ts b/packages/serverless/src/handlers/start.ts index 7fc3726f331..43d3b288c34 100644 --- a/packages/serverless/src/handlers/start.ts +++ b/packages/serverless/src/handlers/start.ts @@ -1,10 +1,13 @@ -import {VERSION} from 'remotion/version'; import type {ServerlessPayload} from '../constants'; import {ServerlessRoutines, overallProgressKey} from '../constants'; import {internalGetOrCreateBucket} from '../get-or-create-bucket'; import {makeInitialOverallRenderProgress} from '../overall-render-progress'; -import type {ProviderSpecifics} from '../provider-implementation'; +import type { + InsideFunctionSpecifics, + ProviderSpecifics, +} from '../provider-implementation'; import type {CloudProvider} from '../types'; +import {checkVersionMismatch} from './check-version-mismatch'; type Options = { expectedBucketOwner: string; @@ -12,26 +15,26 @@ type Options = { renderId: string; }; -export const startHandler = async ( - params: ServerlessPayload, - options: Options, - providerSpecifics: ProviderSpecifics, -) => { +export const startHandler = async ({ + params, + options, + providerSpecifics, + insideFunctionSpecifics, +}: { + params: ServerlessPayload; + options: Options; + providerSpecifics: ProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; +}) => { if (params.type !== ServerlessRoutines.start) { throw new TypeError('Expected type start'); } - if (params.version !== VERSION) { - if (!params.version) { - throw new Error( - `Version mismatch: When calling renderMediaOnLambda(), you called the function ${process.env.AWS_LAMBDA_FUNCTION_NAME} which has the version ${VERSION} but the @remotion/lambda package is an older version. Deploy a new function and use it to call renderMediaOnLambda(). See: https://www.remotion.dev/docs/lambda/upgrading`, - ); - } - - throw new Error( - `Version mismatch: When calling renderMediaOnLambda(), you passed ${process.env.AWS_LAMBDA_FUNCTION_NAME} as the function, which has the version ${VERSION}, but the @remotion/lambda package you used to invoke the function has version ${params.version}. Deploy a new function and use it to call renderMediaOnLambda(). See: https://www.remotion.dev/docs/lambda/upgrading`, - ); - } + checkVersionMismatch({ + apiName: 'renderMediaOnLambda()', + insideFunctionSpecifics, + params, + }); const region = providerSpecifics.getCurrentRegionInFunction(); const bucketName = @@ -72,7 +75,7 @@ export const startHandler = async ( const payload: ServerlessPayload = { type: ServerlessRoutines.launch, - framesPerLambda: params.framesPerLambda, + framesPerFunction: params.framesPerLambda, composition: params.composition, serveUrl: realServeUrl, inputProps: params.inputProps, @@ -96,7 +99,7 @@ export const startHandler = async ( scale: params.scale, numberOfGifLoops: params.numberOfGifLoops, everyNthFrame: params.everyNthFrame, - concurrencyPerLambda: params.concurrencyPerLambda, + concurrencyPerFunction: params.concurrencyPerLambda, downloadBehavior: params.downloadBehavior, muted: params.muted, overwrite: params.overwrite, @@ -118,7 +121,7 @@ export const startHandler = async ( }; await providerSpecifics.callFunctionAsync({ - functionName: providerSpecifics.getCurrentFunctionName(), + functionName: insideFunctionSpecifics.getCurrentFunctionName(), type: ServerlessRoutines.launch, payload, region, diff --git a/packages/serverless/src/handlers/still.ts b/packages/serverless/src/handlers/still.ts index 3733f98ff43..a94eb4b0a7a 100644 --- a/packages/serverless/src/handlers/still.ts +++ b/packages/serverless/src/handlers/still.ts @@ -22,21 +22,22 @@ import {internalGetOrCreateBucket} from '../get-or-create-bucket'; import {onDownloadsHelper} from '../on-downloads-helpers'; import {makeInitialOverallRenderProgress} from '../overall-render-progress'; import type { + InsideFunctionSpecifics, ProviderSpecifics, - ServerProviderSpecifics, } from '../provider-implementation'; import type {RenderMetadata} from '../render-metadata'; import type {OnStream} from '../streaming/streaming'; import type { CloudProvider, ReceivedArtifact, - RenderStillLambdaResponsePayload, + RenderStillFunctionResponsePayload, } from '../types'; import {validateComposition} from '../validate-composition'; import {validateDownloadBehavior} from '../validate-download-behavior'; import {validateOutname} from '../validate-outname'; import {validatePrivacy} from '../validate-privacy'; -import {getTmpDirStateIfENoSp} from '../write-lambda-error'; +import {getTmpDirStateIfENoSp} from '../write-error-to-storage'; +import {checkVersionMismatch} from './check-version-mismatch'; type Options = { params: ServerlessPayload; @@ -45,41 +46,35 @@ type Options = { onStream: OnStream; timeoutInMilliseconds: number; providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; }; const innerStillHandler = async ( { - params: lambdaParams, + params, expectedBucketOwner, renderId, onStream, timeoutInMilliseconds, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }: Options, cleanup: CleanupFn[], ) => { - if (lambdaParams.type !== ServerlessRoutines.still) { + if (params.type !== ServerlessRoutines.still) { throw new TypeError('Expected still type'); } - if (lambdaParams.version !== VERSION) { - if (!lambdaParams.version) { - throw new Error( - `Version mismatch: When calling renderStillOnLambda(), you called the function ${process.env.AWS_LAMBDA_FUNCTION_NAME} which has the version ${VERSION} but the @remotion/lambda package is an older version. Deploy a new function and use it to call renderStillOnLambda(). See: https://www.remotion.dev/docs/lambda/upgrading`, - ); - } - - throw new Error( - `Version mismatch: When calling renderStillOnLambda(), you passed ${process.env.AWS_LAMBDA_FUNCTION_NAME} as the function, which has the version ${VERSION}, but the @remotion/lambda package you used to invoke the function has version ${lambdaParams.version}. Deploy a new function and use it to call renderStillOnLambda(). See: https://www.remotion.dev/docs/lambda/upgrading`, - ); - } + checkVersionMismatch({ + apiName: 'renderStillOnLambda()', + insideFunctionSpecifics, + params, + }); - validateDownloadBehavior(lambdaParams.downloadBehavior); - validatePrivacy(lambdaParams.privacy, true); + validateDownloadBehavior(params.downloadBehavior); + validatePrivacy(params.privacy, true); validateOutname({ - outName: lambdaParams.outName, + outName: params.outName, codec: null, audioCodecSetting: null, separateAudioTo: null, @@ -87,21 +82,21 @@ const innerStillHandler = async ( const start = Date.now(); - const browserInstancePromise = serverProviderSpecifics.getBrowserInstance({ - logLevel: lambdaParams.logLevel, + const browserInstancePromise = insideFunctionSpecifics.getBrowserInstance({ + logLevel: params.logLevel, indent: false, - chromiumOptions: lambdaParams.chromiumOptions, + chromiumOptions: params.chromiumOptions, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }); const bucketNamePromise = - lambdaParams.bucketName ?? + params.bucketName ?? internalGetOrCreateBucket({ region: providerSpecifics.getCurrentRegionInFunction(), enableFolderExpiry: null, customCredentials: null, providerSpecifics, - forcePathStyle: lambdaParams.forcePathStyle, + forcePathStyle: params.forcePathStyle, skipPutAcl: false, }).then((b) => b.bucketName); @@ -115,14 +110,14 @@ const innerStillHandler = async ( bucketName, expectedBucketOwner, region, - serialized: lambdaParams.inputProps, + serialized: params.inputProps, propsType: 'input-props', providerSpecifics, - forcePathStyle: lambdaParams.forcePathStyle, + forcePathStyle: params.forcePathStyle, }); const serveUrl = providerSpecifics.convertToServeUrl({ - urlOrId: lambdaParams.serveUrl, + urlOrId: params.serveUrl, region, bucketName, }); @@ -134,10 +129,9 @@ const innerStillHandler = async ( indent: false, port: null, remotionRoot: process.cwd(), - logLevel: lambdaParams.logLevel, + logLevel: params.logLevel, webpackConfigOrServeUrl: serveUrl, - offthreadVideoCacheSizeInBytes: - lambdaParams.offthreadVideoCacheSizeInBytes, + offthreadVideoCacheSizeInBytes: params.offthreadVideoCacheSizeInBytes, binariesDirectory: null, forceIPv4: false, }, @@ -152,17 +146,17 @@ const innerStillHandler = async ( const composition = await validateComposition({ serveUrl, browserInstance: browserInstance.instance, - composition: lambdaParams.composition, + composition: params.composition, serializedInputPropsWithCustomSchema, - envVariables: lambdaParams.envVariables ?? {}, - chromiumOptions: lambdaParams.chromiumOptions, - timeoutInMilliseconds: lambdaParams.timeoutInMilliseconds, + envVariables: params.envVariables ?? {}, + chromiumOptions: params.chromiumOptions, + timeoutInMilliseconds: params.timeoutInMilliseconds, port: null, - forceHeight: lambdaParams.forceHeight, - forceWidth: lambdaParams.forceWidth, - logLevel: lambdaParams.logLevel, + forceHeight: params.forceHeight, + forceWidth: params.forceWidth, + logLevel: params.logLevel, server, - offthreadVideoCacheSizeInBytes: lambdaParams.offthreadVideoCacheSizeInBytes, + offthreadVideoCacheSizeInBytes: params.offthreadVideoCacheSizeInBytes, onBrowserDownload: () => { throw new Error('Should not download a browser in Lambda'); }, @@ -173,31 +167,31 @@ const innerStillHandler = async ( const renderMetadata: RenderMetadata = { startedDate: Date.now(), codec: null, - compositionId: lambdaParams.composition, + compositionId: params.composition, estimatedTotalLambdaInvokations: 1, estimatedRenderLambdaInvokations: 1, siteId: serveUrl, totalChunks: 1, type: 'still', - imageFormat: lambdaParams.imageFormat, - inputProps: lambdaParams.inputProps, + imageFormat: params.imageFormat, + inputProps: params.inputProps, lambdaVersion: VERSION, framesPerLambda: 1, - memorySizeInMb: Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE), + memorySizeInMb: insideFunctionSpecifics.getCurrentMemorySizeInMb(), region: providerSpecifics.getCurrentRegionInFunction(), renderId, - outName: lambdaParams.outName ?? undefined, - privacy: lambdaParams.privacy, + outName: params.outName ?? undefined, + privacy: params.privacy, audioCodec: null, - deleteAfter: lambdaParams.deleteAfter, + deleteAfter: params.deleteAfter, numberOfGifLoops: null, - downloadBehavior: lambdaParams.downloadBehavior, + downloadBehavior: params.downloadBehavior, audioBitrate: null, metadata: null, - functionName: process.env.AWS_LAMBDA_FUNCTION_NAME as string, + functionName: insideFunctionSpecifics.getCurrentFunctionName(), dimensions: { - height: composition.height * (lambdaParams.scale ?? 1), - width: composition.width * (lambdaParams.scale ?? 1), + height: composition.height * (params.scale ?? 1), + width: composition.width * (params.scale ?? 1), }, }; @@ -213,7 +207,7 @@ const innerStillHandler = async ( expectedBucketOwner, downloadBehavior: null, customCredentials: null, - forcePathStyle: lambdaParams.forcePathStyle, + forcePathStyle: params.forcePathStyle, }); const onBrowserDownload = () => { @@ -225,7 +219,7 @@ const innerStillHandler = async ( const {key, renderBucketName, customCredentials} = getExpectedOutName( renderMetadata, bucketName, - getCredentialsFromOutName(lambdaParams.outName), + getCredentialsFromOutName(params.outName), ); const onArtifact = (artifact: EmittedArtifact): {alreadyExisted: boolean} => { @@ -246,7 +240,7 @@ const innerStillHandler = async ( const startTime = Date.now(); RenderInternals.Log.info( - {indent: false, logLevel: lambdaParams.logLevel}, + {indent: false, logLevel: params.logLevel}, 'Writing artifact ' + artifact.filename + ' to S3', ); providerSpecifics @@ -255,21 +249,21 @@ const innerStillHandler = async ( key: storageKey, body: artifact.content, region, - privacy: lambdaParams.privacy, + privacy: params.privacy, expectedBucketOwner, - downloadBehavior: lambdaParams.downloadBehavior, + downloadBehavior: params.downloadBehavior, customCredentials, - forcePathStyle: lambdaParams.forcePathStyle, + forcePathStyle: params.forcePathStyle, }) .then(() => { RenderInternals.Log.info( - {indent: false, logLevel: lambdaParams.logLevel}, + {indent: false, logLevel: params.logLevel}, `Wrote artifact to S3 in ${Date.now() - startTime}ms`, ); }) .catch((err) => { RenderInternals.Log.error( - {indent: false, logLevel: lambdaParams.logLevel}, + {indent: false, logLevel: params.logLevel}, 'Failed to write artifact to S3', err, ); @@ -281,35 +275,34 @@ const innerStillHandler = async ( composition, output: outputPath, serveUrl, - envVariables: lambdaParams.envVariables ?? {}, + envVariables: params.envVariables ?? {}, frame: RenderInternals.convertToPositiveFrameIndex({ - frame: lambdaParams.frame, + frame: params.frame, durationInFrames: composition.durationInFrames, }), - imageFormat: lambdaParams.imageFormat as StillImageFormat, + imageFormat: params.imageFormat as StillImageFormat, serializedInputPropsWithCustomSchema, overwrite: false, puppeteerInstance: browserInstance.instance, - jpegQuality: - lambdaParams.jpegQuality ?? RenderInternals.DEFAULT_JPEG_QUALITY, - chromiumOptions: lambdaParams.chromiumOptions, - scale: lambdaParams.scale, - timeoutInMilliseconds: lambdaParams.timeoutInMilliseconds, + jpegQuality: params.jpegQuality ?? RenderInternals.DEFAULT_JPEG_QUALITY, + chromiumOptions: params.chromiumOptions, + scale: params.scale, + timeoutInMilliseconds: params.timeoutInMilliseconds, browserExecutable: providerSpecifics.getChromiumPath(), cancelSignal: null, indent: false, onBrowserLog: null, - onDownload: onDownloadsHelper(lambdaParams.logLevel), + onDownload: onDownloadsHelper(params.logLevel), port: null, server, - logLevel: lambdaParams.logLevel, + logLevel: params.logLevel, serializedResolvedPropsWithCustomSchema: NoReactInternals.serializeJSONWithDate({ indent: undefined, staticBase: null, data: composition.props, }).serializedString, - offthreadVideoCacheSizeInBytes: lambdaParams.offthreadVideoCacheSizeInBytes, + offthreadVideoCacheSizeInBytes: params.offthreadVideoCacheSizeInBytes, binariesDirectory: null, onBrowserDownload, onArtifact, @@ -320,29 +313,29 @@ const innerStillHandler = async ( await providerSpecifics.writeFile({ bucketName: renderBucketName, key, - privacy: lambdaParams.privacy, + privacy: params.privacy, body: fs.createReadStream(outputPath), expectedBucketOwner, region: providerSpecifics.getCurrentRegionInFunction(), - downloadBehavior: lambdaParams.downloadBehavior, + downloadBehavior: params.downloadBehavior, customCredentials, - forcePathStyle: lambdaParams.forcePathStyle, + forcePathStyle: params.forcePathStyle, }); await Promise.all([ fs.promises.rm(outputPath, {recursive: true}), cleanupSerializedInputProps({ region: providerSpecifics.getCurrentRegionInFunction(), - serialized: lambdaParams.inputProps, + serialized: params.inputProps, providerSpecifics, - forcePathStyle: lambdaParams.forcePathStyle, + forcePathStyle: params.forcePathStyle, }), server.closeServer(true), ]); const estimatedPrice = providerSpecifics.estimatePrice({ durationInMilliseconds: Date.now() - start + 100, - memorySizeInMb: Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE), + memorySizeInMb: insideFunctionSpecifics.getCurrentMemorySizeInMb(), region: providerSpecifics.getCurrentRegionInFunction(), lambdasInvoked: 1, diskSizeInMb: providerSpecifics.getEphemeralStorageForPriceCalculation(), @@ -355,7 +348,7 @@ const innerStillHandler = async ( currentRegion: providerSpecifics.getCurrentRegionInFunction(), }); - const payload: RenderStillLambdaResponsePayload = { + const payload: RenderStillFunctionResponsePayload = { type: 'success' as const, output: url, size, @@ -448,7 +441,7 @@ export const stillHandler = async ( stack: (err as Error).stack as string, }; } finally { - options.serverProviderSpecifics.forgetBrowserEventLoop( + options.insideFunctionSpecifics.forgetBrowserEventLoop( options.params.type === ServerlessRoutines.still ? options.params.logLevel : 'error', diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index a07fd78648f..2b3e8107e1c 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -1,5 +1,5 @@ export {PostRenderData, ServerlessRoutines} from './constants'; -export {estimatePriceFromBucket} from './estimate-price-from-bucket'; +export {estimatePriceFromMetadata as estimatePriceFromBucket} from './estimate-price-from-bucket'; export {getCredentialsFromOutName} from './expected-out-name'; export {formatCostsInfo} from './format-costs-info'; export { @@ -13,6 +13,7 @@ export {RequestContext, rendererHandler} from './handlers/renderer'; export {startHandler} from './handlers/start'; export {stillHandler} from './handlers/still'; export {infoHandler} from './info'; +export {innerHandler} from './inner-routine'; export {inspectErrors} from './inspect-error'; export {WebhookClient, WebhookPayload, invokeWebhook} from './invoke-webhook'; export {setCurrentRequestId, stopLeakDetection} from './leak-detection'; @@ -47,6 +48,6 @@ export { } from './validate-webhook'; export { EnhancedErrorInfo, - LambdaErrorInfo, + FunctionErrorInfo as LambdaErrorInfo, getTmpDirStateIfENoSp, -} from './write-lambda-error'; +} from './write-error-to-storage'; diff --git a/packages/serverless/src/inner-routine.ts b/packages/serverless/src/inner-routine.ts new file mode 100644 index 00000000000..69d2f176cde --- /dev/null +++ b/packages/serverless/src/inner-routine.ts @@ -0,0 +1,372 @@ +import {RenderInternals} from '@remotion/renderer'; +import {makeStreamPayload} from './client'; +import type {ServerlessPayload} from './constants'; +import {COMMAND_NOT_FOUND, ServerlessRoutines} from './constants'; +import {compositionsHandler} from './handlers/compositions'; +import {launchHandler} from './handlers/launch'; +import {progressHandler} from './handlers/progress'; +import type {RequestContext} from './handlers/renderer'; +import {rendererHandler} from './handlers/renderer'; +import {startHandler} from './handlers/start'; +import {stillHandler} from './handlers/still'; +import {infoHandler} from './info'; +import type {WebhookClient} from './invoke-webhook'; +import {getWarm, setWarm} from './is-warm'; +import {setCurrentRequestId, stopLeakDetection} from './leak-detection'; +import {printLoggingGrepHelper} from './print-logging-grep-helper'; +import type { + InsideFunctionSpecifics, + ProviderSpecifics, +} from './provider-implementation'; +import type {OrError} from './return-values'; +import type {ResponseStreamWriter} from './streaming/stream-writer'; +import type {StreamingPayload} from './streaming/streaming'; +import type {CloudProvider} from './types'; + +export const innerHandler = async ({ + params, + responseWriter, + context, + providerSpecifics, + insideFunctionSpecifics, + webhookClient, +}: { + params: ServerlessPayload; + responseWriter: ResponseStreamWriter; + context: RequestContext; + providerSpecifics: ProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; + webhookClient: WebhookClient; +}): Promise => { + setCurrentRequestId(context.awsRequestId); + process.env.__RESERVED_IS_INSIDE_REMOTION_LAMBDA = 'true'; + const timeoutInMilliseconds = context.getRemainingTimeInMillis(); + + RenderInternals.Log.verbose( + {indent: false, logLevel: params.logLevel}, + 'AWS Request ID:', + context.awsRequestId, + ); + stopLeakDetection(); + if (!context?.invokedFunctionArn) { + throw new Error( + 'Lambda function unexpectedly does not have context.invokedFunctionArn', + ); + } + + await insideFunctionSpecifics.deleteTmpDir(); + const isWarm = getWarm(); + setWarm(); + + const currentUserId = context.invokedFunctionArn.split(':')[4]; + if (params.type === ServerlessRoutines.still) { + providerSpecifics.validateDeleteAfter(params.deleteAfter); + const renderId = insideFunctionSpecifics.generateRandomId({ + deleteAfter: params.deleteAfter, + randomHashFn: providerSpecifics.randomHash, + }); + if (providerSpecifics.printLoggingHelper) { + printLoggingGrepHelper( + ServerlessRoutines.still, + { + renderId, + inputProps: JSON.stringify(params.inputProps), + isWarm, + }, + params.logLevel, + ); + } + + try { + await new Promise((resolve, reject) => { + const onStream = async (payload: StreamingPayload) => { + if (!params.streamed) { + if (payload.type !== 'still-rendered') { + throw new Error('Expected still-rendered'); + } + + await responseWriter.write( + Buffer.from(JSON.stringify(payload.payload)), + ); + return; + } + + const message = makeStreamPayload({ + message: payload, + }); + return new Promise((innerResolve, innerReject) => { + responseWriter + .write(message) + .then(() => { + innerResolve(); + }) + .catch((err) => { + reject(err); + innerReject(err); + }); + }); + }; + + if (params.streamed) { + onStream({ + type: 'render-id-determined', + payload: {renderId}, + }); + } + + stillHandler({ + expectedBucketOwner: currentUserId, + params, + renderId, + onStream, + timeoutInMilliseconds, + providerSpecifics, + insideFunctionSpecifics, + }) + .then((r) => { + resolve(r); + }) + .catch((err) => { + reject(err); + }); + }); + await responseWriter.end(); + } catch (err) { + // eslint-disable-next-line no-console + console.log({err}); + } + + return; + } + + if (params.type === ServerlessRoutines.start) { + const renderId = insideFunctionSpecifics.generateRandomId({ + deleteAfter: params.deleteAfter, + randomHashFn: providerSpecifics.randomHash, + }); + + if (providerSpecifics.printLoggingHelper) { + printLoggingGrepHelper( + ServerlessRoutines.start, + { + renderId, + inputProps: JSON.stringify(params.inputProps), + isWarm, + }, + params.logLevel, + ); + } + + const response = await startHandler({ + params, + options: { + expectedBucketOwner: currentUserId, + timeoutInMilliseconds, + renderId, + }, + providerSpecifics, + insideFunctionSpecifics, + }); + + await responseWriter.write(Buffer.from(JSON.stringify(response))); + await responseWriter.end(); + return; + } + + if (params.type === ServerlessRoutines.launch) { + if (providerSpecifics.printLoggingHelper) { + printLoggingGrepHelper( + ServerlessRoutines.launch, + { + renderId: params.renderId, + inputProps: JSON.stringify(params.inputProps), + isWarm, + }, + params.logLevel, + ); + } + + const response = await launchHandler({ + params, + options: { + expectedBucketOwner: currentUserId, + getRemainingTimeInMillis: context.getRemainingTimeInMillis, + }, + providerSpecifics, + client: webhookClient, + insideFunctionSpecifics, + }); + + await responseWriter.write(Buffer.from(JSON.stringify(response))); + await responseWriter.end(); + return; + } + + if (params.type === ServerlessRoutines.status) { + if (providerSpecifics.printLoggingHelper) { + printLoggingGrepHelper( + ServerlessRoutines.status, + { + renderId: params.renderId, + isWarm, + }, + params.logLevel, + ); + } + + const response = await progressHandler({ + params, + options: { + expectedBucketOwner: currentUserId, + timeoutInMilliseconds, + retriesRemaining: 2, + providerSpecifics, + insideFunctionSpecifics, + }, + }); + + await responseWriter.write(Buffer.from(JSON.stringify(response))); + await responseWriter.end(); + return; + } + + if (params.type === ServerlessRoutines.renderer) { + if (providerSpecifics.printLoggingHelper) { + printLoggingGrepHelper( + ServerlessRoutines.renderer, + { + renderId: params.renderId, + chunk: String(params.chunk), + dumpLogs: String( + RenderInternals.isEqualOrBelowLogLevel(params.logLevel, 'verbose'), + ), + resolvedProps: JSON.stringify(params.resolvedProps), + isWarm, + }, + params.logLevel, + ); + } + + await new Promise((resolve, reject) => { + rendererHandler({ + params, + options: { + expectedBucketOwner: currentUserId, + isWarm, + }, + onStream: (payload) => { + const message = makeStreamPayload({ + message: payload, + }); + + const writeProm = responseWriter.write(message); + + return new Promise((innerResolve, innerReject) => { + writeProm + .then(() => { + innerResolve(); + }) + .catch((err) => { + reject(err); + innerReject(err); + }); + }); + }, + requestContext: context, + providerSpecifics, + insideFunctionSpecifics, + }) + .then((res) => { + resolve(res); + }) + .catch((err) => { + reject(err); + }); + }); + + await responseWriter.end(); + + return; + } + + if (params.type === ServerlessRoutines.info) { + if (providerSpecifics.printLoggingHelper) { + printLoggingGrepHelper( + ServerlessRoutines.info, + { + isWarm, + }, + params.logLevel, + ); + } + + const response = await infoHandler(params); + await responseWriter.write(Buffer.from(JSON.stringify(response))); + await responseWriter.end(); + return; + } + + if (params.type === ServerlessRoutines.compositions) { + if (providerSpecifics.printLoggingHelper) { + printLoggingGrepHelper( + ServerlessRoutines.compositions, + { + isWarm, + }, + params.logLevel, + ); + } + + const response = await compositionsHandler({ + params, + options: { + expectedBucketOwner: currentUserId, + }, + providerSpecifics, + insideFunctionSpecifics, + }); + + await responseWriter.write(Buffer.from(JSON.stringify(response))); + await responseWriter.end(); + + return; + } + + throw new Error(COMMAND_NOT_FOUND); +}; + +export const innerRoutine = async ({ + params, + responseWriter, + context, + providerSpecifics, + insideFunctionSpecifics, + webhookClient, +}: { + params: ServerlessPayload; + responseWriter: ResponseStreamWriter; + context: RequestContext; + providerSpecifics: ProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; + webhookClient: WebhookClient; +}): Promise => { + try { + await innerHandler({ + params, + responseWriter, + context, + providerSpecifics, + insideFunctionSpecifics, + webhookClient, + }); + } catch (err) { + const res: OrError<0> = { + type: 'error', + message: (err as Error).message, + stack: (err as Error).stack as string, + }; + + await responseWriter.write(Buffer.from(JSON.stringify(res))); + await responseWriter.end(); + } +}; diff --git a/packages/serverless/src/inspect-error.ts b/packages/serverless/src/inspect-error.ts index 1bbd4b9e930..a3d2947163b 100644 --- a/packages/serverless/src/inspect-error.ts +++ b/packages/serverless/src/inspect-error.ts @@ -4,7 +4,10 @@ import { isBrowserCrashedError, isErrInsufficientResourcesErr, } from './error-category'; -import type {EnhancedErrorInfo, LambdaErrorInfo} from './write-lambda-error'; +import type { + EnhancedErrorInfo, + FunctionErrorInfo, +} from './write-error-to-storage'; const FAILED_TO_LAUNCH_TOKEN = 'Failed to launch browser.'; @@ -45,7 +48,7 @@ const getExplanation = (stack: string) => { export const inspectErrors = ({ errors, }: { - errors: LambdaErrorInfo[]; + errors: FunctionErrorInfo[]; }): EnhancedErrorInfo[] => { return errors.map((e): EnhancedErrorInfo => { return { diff --git a/packages/serverless/src/invoke-webhook.ts b/packages/serverless/src/invoke-webhook.ts index 2bfda4ae8c6..9fd032320f6 100644 --- a/packages/serverless/src/invoke-webhook.ts +++ b/packages/serverless/src/invoke-webhook.ts @@ -4,7 +4,7 @@ import type https from 'https'; import * as Crypto from 'node:crypto'; import type http from 'node:http'; import type {AfterRenderCost} from './constants'; -import type {EnhancedErrorInfo} from './write-lambda-error'; +import type {EnhancedErrorInfo} from './write-error-to-storage'; export function calculateSignature(payload: string, secret: string | null) { if (!secret) { @@ -49,6 +49,8 @@ export type WebhookPayload = { const redirectStatusCodes = [301, 302, 303, 307, 308]; export type WebhookClient = ( + url: string, +) => ( url: string | URL, options: https.RequestOptions, callback?: (res: http.IncomingMessage) => void, @@ -72,7 +74,7 @@ function invokeWebhookRaw({ const jsonPayload = JSON.stringify(payload); return new Promise((resolve, reject) => { - const req = client( + const req = client(url)( url, { method: 'POST', diff --git a/packages/lambda/src/functions/helpers/is-warm.ts b/packages/serverless/src/is-warm.ts similarity index 100% rename from packages/lambda/src/functions/helpers/is-warm.ts rename to packages/serverless/src/is-warm.ts diff --git a/packages/serverless/src/make-timeout-error.ts b/packages/serverless/src/make-timeout-error.ts index 24ef43655ce..2d310ddfebb 100644 --- a/packages/serverless/src/make-timeout-error.ts +++ b/packages/serverless/src/make-timeout-error.ts @@ -2,7 +2,7 @@ import {makeTimeoutMessage} from './make-timeout-message'; import type {ProviderSpecifics} from './provider-implementation'; import type {RenderMetadata} from './render-metadata'; import type {CloudProvider} from './types'; -import type {EnhancedErrorInfo} from './write-lambda-error'; +import type {EnhancedErrorInfo} from './write-error-to-storage'; export const makeTimeoutError = ({ timeoutInMilliseconds, diff --git a/packages/serverless/src/make-timeout-message.ts b/packages/serverless/src/make-timeout-message.ts index 71846b67448..e0920602b98 100644 --- a/packages/serverless/src/make-timeout-message.ts +++ b/packages/serverless/src/make-timeout-message.ts @@ -11,11 +11,13 @@ const makeChunkMissingMessage = ({ renderMetadata, region, providerSpecifics, + functionName, }: { missingChunks: number[]; renderMetadata: RenderMetadata; region: Provider['region']; providerSpecifics: ProviderSpecifics; + functionName: string; }) => { if (missingChunks.length === 0) { return 'All chunks have been successfully rendered, but the main function has timed out.'; @@ -43,7 +45,7 @@ const makeChunkMissingMessage = ({ msg, `▸ Logs for chunk ${ch}: ${providerSpecifics.getLoggingUrlForRendererFunction( { - functionName: providerSpecifics.getCurrentFunctionName(), + functionName, region, rendererFunctionName: null, renderId: renderMetadata.renderId, @@ -96,6 +98,7 @@ export const makeTimeoutMessage = ({ renderMetadata, region, providerSpecifics, + functionName, }), '', `Consider increasing the timeout of your function.`, diff --git a/packages/serverless/src/merge-chunks.ts b/packages/serverless/src/merge-chunks.ts index 5472f4a0d52..84ac929ea50 100644 --- a/packages/serverless/src/merge-chunks.ts +++ b/packages/serverless/src/merge-chunks.ts @@ -15,8 +15,8 @@ import {createPostRenderData} from './create-post-render-data'; import {inspectErrors} from './inspect-error'; import type {OverallProgressHelper} from './overall-render-progress'; import type { + InsideFunctionSpecifics, ProviderSpecifics, - ServerProviderSpecifics, } from './provider-implementation'; import type {RenderMetadata} from './render-metadata'; import type {CloudProvider} from './types'; @@ -52,7 +52,7 @@ export const mergeChunksAndFinishRender = async < overallProgress: OverallProgressHelper; startTime: number; providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; forcePathStyle: boolean; }): Promise> => { const onProgress = (framesEncoded: number) => { @@ -85,14 +85,14 @@ export const mergeChunksAndFinishRender = async < preferLossless: options.preferLossless, muted: options.renderMetadata.muted, metadata: options.renderMetadata.metadata, - serverProviderSpecifics: options.serverProviderSpecifics, + insideFunctionSpecifics: options.insideFunctionSpecifics, }); const encodingStop = Date.now(); options.overallProgress.setTimeToCombine(encodingStop - encodingStart); const outputSize = fs.statSync(outfile).size; - const writeToBucket = options.serverProviderSpecifics.timer( + const writeToBucket = options.insideFunctionSpecifics.timer( `Writing to bucket (${outputSize} bytes)`, options.logLevel, ); @@ -131,7 +131,7 @@ export const mergeChunksAndFinishRender = async < const postRenderData = createPostRenderData({ region: options.providerSpecifics.getCurrentRegionInFunction(), - memorySizeInMb: Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE), + memorySizeInMb: options.insideFunctionSpecifics.getCurrentMemorySizeInMb(), renderMetadata: options.renderMetadata, errorExplanations, timeToDelete: (await cleanupProm).reduce((a, b) => Math.max(a, b), 0), diff --git a/packages/serverless/src/most-expensive-chunks.ts b/packages/serverless/src/most-expensive-chunks.ts index 412425fd8ab..a3045852530 100644 --- a/packages/serverless/src/most-expensive-chunks.ts +++ b/packages/serverless/src/most-expensive-chunks.ts @@ -10,12 +10,12 @@ export type ExpensiveChunk = { export const getMostExpensiveChunks = ({ parsedTimings, - framesPerLambda, + framesPerFunction: framesPerLambda, firstFrame, lastFrame, }: { parsedTimings: ParsedTiming[]; - framesPerLambda: number; + framesPerFunction: number; firstFrame: number; lastFrame: number; }): ExpensiveChunk[] => { diff --git a/packages/serverless/src/overall-render-progress.ts b/packages/serverless/src/overall-render-progress.ts index 3ca94d2e17f..b6184955de8 100644 --- a/packages/serverless/src/overall-render-progress.ts +++ b/packages/serverless/src/overall-render-progress.ts @@ -10,7 +10,7 @@ import type { ParsedTiming, ReceivedArtifact, } from './types'; -import type {LambdaErrorInfo} from './write-lambda-error'; +import type {FunctionErrorInfo} from './write-error-to-storage'; export type OverallRenderProgress = { chunks: number[]; @@ -25,7 +25,7 @@ export type OverallRenderProgress = { postRenderData: PostRenderData | null; timings: ParsedTiming[]; renderMetadata: RenderMetadata | null; - errors: LambdaErrorInfo[]; + errors: FunctionErrorInfo[]; timeoutTimestamp: number; functionLaunched: number; serveUrlOpened: number | null; @@ -55,7 +55,7 @@ export type OverallProgressHelper = { addRetry: (retry: ChunkRetry) => void; setPostRenderData: (postRenderData: PostRenderData) => void; setRenderMetadata: (renderMetadata: RenderMetadata) => void; - addErrorWithoutUpload: (errorInfo: LambdaErrorInfo) => void; + addErrorWithoutUpload: (errorInfo: FunctionErrorInfo) => void; setExpectedChunks: (expectedChunks: number) => void; get: () => OverallRenderProgress; setServeUrlOpened: (timestamp: number) => void; diff --git a/packages/serverless/src/plan-frame-ranges.ts b/packages/serverless/src/plan-frame-ranges.ts index 75492f48b5c..4f2a91b6c97 100644 --- a/packages/serverless/src/plan-frame-ranges.ts +++ b/packages/serverless/src/plan-frame-ranges.ts @@ -1,11 +1,11 @@ import {RenderInternals} from '@remotion/renderer'; export const planFrameRanges = ({ - framesPerLambda, + framesPerFunction, frameRange, everyNthFrame, }: { - framesPerLambda: number; + framesPerFunction: number; frameRange: [number, number]; everyNthFrame: number; }): {chunks: [number, number][]} => { @@ -13,15 +13,15 @@ export const planFrameRanges = ({ frameRange, everyNthFrame, ); - const chunkCount = Math.ceil(framesToRender.length / framesPerLambda); + const chunkCount = Math.ceil(framesToRender.length / framesPerFunction); const firstFrame = frameRange[0]; return { chunks: new Array(chunkCount).fill(1).map((_, i) => { - const start = i * framesPerLambda * everyNthFrame + firstFrame; + const start = i * framesPerFunction * everyNthFrame + firstFrame; const end = Math.min( framesToRender[framesToRender.length - 1], - (i + 1) * framesPerLambda * everyNthFrame - 1 + firstFrame, + (i + 1) * framesPerFunction * everyNthFrame - 1 + firstFrame, ); return [start, end]; diff --git a/packages/lambda/src/functions/helpers/print-logging-helper.ts b/packages/serverless/src/print-logging-grep-helper.ts similarity index 88% rename from packages/lambda/src/functions/helpers/print-logging-helper.ts rename to packages/serverless/src/print-logging-grep-helper.ts index 07abe2fa1f8..021aacd4eee 100644 --- a/packages/lambda/src/functions/helpers/print-logging-helper.ts +++ b/packages/serverless/src/print-logging-grep-helper.ts @@ -1,6 +1,6 @@ import type {LogLevel} from '@remotion/renderer'; import {RenderInternals} from '@remotion/renderer'; -import type {ServerlessRoutines} from '@remotion/serverless/client'; +import type {ServerlessRoutines} from './constants'; export type PrintLoggingHelper = ( type: ServerlessRoutines, diff --git a/packages/serverless/src/progress.ts b/packages/serverless/src/progress.ts index 1e15de906f7..f0704efb89b 100644 --- a/packages/serverless/src/progress.ts +++ b/packages/serverless/src/progress.ts @@ -3,7 +3,7 @@ import {NoReactAPIs} from '@remotion/renderer/pure'; import {NoReactInternals} from 'remotion/no-react'; import {calculateChunkTimes} from './calculate-chunk-times'; import type {CustomCredentials} from './constants'; -import {estimatePriceFromBucket} from './estimate-price-from-bucket'; +import {estimatePriceFromMetadata} from './estimate-price-from-bucket'; import {getExpectedOutName} from './expected-out-name'; import {formatCostsInfo} from './format-costs-info'; import {getOverallProgress} from './get-overall-progress'; @@ -15,7 +15,7 @@ import {lambdaRenderHasAudioVideo} from './render-has-audio-video'; import type {CleanupInfo, GenericRenderProgress} from './render-progress'; import {truthy} from './truthy'; import type {CloudProvider} from './types'; -import type {EnhancedErrorInfo} from './write-lambda-error'; +import type {EnhancedErrorInfo} from './write-error-to-storage'; export const getProgress = async ({ bucketName, @@ -197,10 +197,10 @@ export const getProgress = async ({ }; } - const priceFromBucket = estimatePriceFromBucket({ + const priceFromBucket = estimatePriceFromMetadata({ renderMetadata, memorySizeInMb, - lambdasInvoked: renderMetadata.estimatedRenderLambdaInvokations ?? 0, + functionsInvoked: renderMetadata.estimatedRenderLambdaInvokations ?? 0, diskSizeInMb: providerSpecifics.getEphemeralStorageForPriceCalculation(), timings: overallProgress.timings ?? [], region, diff --git a/packages/serverless/src/provider-implementation.ts b/packages/serverless/src/provider-implementation.ts index 1c3976d3743..da2aeb26c23 100644 --- a/packages/serverless/src/provider-implementation.ts +++ b/packages/serverless/src/provider-implementation.ts @@ -1,3 +1,4 @@ +import type {bundle} from '@remotion/bundler'; import type { ChromiumOptions, EmittedArtifact, @@ -6,6 +7,7 @@ import type { import type {Readable} from 'stream'; import type { CustomCredentials, + DeleteAfter, DownloadBehavior, Privacy, ServerlessRoutines, @@ -216,21 +218,118 @@ export type GetBrowserInstance = ({ indent, chromiumOptions, providerSpecifics, - serverProviderSpecifics, + insideFunctionSpecifics, }: { logLevel: LogLevel; indent: boolean; chromiumOptions: ChromiumOptions; providerSpecifics: ProviderSpecifics; - serverProviderSpecifics: ServerProviderSpecifics; + insideFunctionSpecifics: InsideFunctionSpecifics; }) => Promise; export type ForgetBrowserEventLoop = (logLevel: LogLevel) => void; -export type ServerProviderSpecifics = { +export type GenerateRenderId = (options: { + deleteAfter: DeleteAfter | null; + randomHashFn: () => string; +}) => string; + +export type GetAccountId = (options: { + region: Provider['region']; +}) => Promise; + +export type DeleteFunctionInput = { + region: Provider['region']; + functionName: string; +}; + +export type DeleteFunction = ( + options: DeleteFunctionInput, +) => Promise; + +export type GetFunctionsInput = { + region: Provider['region']; + compatibleOnly: boolean; + logLevel?: LogLevel; +}; + +export type FunctionInfo = { + functionName: string; + timeoutInSeconds: number; + memorySizeInMb: number; + version: string | null; + diskSizeInMb: number; +}; + +export type GetFunctions = ( + params: GetFunctionsInput, +) => Promise; + +export type ReadDir = ({ + dir, + etags, + originalDir, + onProgress, +}: { + dir: string; + etags: { + [key: string]: () => Promise; + }; + originalDir: string; + onProgress: (bytes: number) => void; +}) => { + [key: string]: () => Promise; +}; + +export type UploadDirProgress = { + totalFiles: number; + filesUploaded: number; + totalSize: number; + sizeUploaded: number; +}; + +export type UploadDir = (options: { + bucket: string; + region: Provider['region']; + localDir: string; + keyPrefix: string; + onProgress: (progress: UploadDirProgress) => void; + privacy: Privacy; + toUpload: string[]; + forcePathStyle: boolean; +}) => Promise; + +type CreateFunctionOptions = { + region: Provider['region']; + logLevel: LogLevel; + ephemerealStorageInMb: number; + timeoutInSeconds: number; + memorySizeInMb: number; + functionName: string; + zipFile: string; +} & Provider['creationFunctionOptions']; + +export type CreateFunction = ( + options: CreateFunctionOptions, +) => Promise<{FunctionName: string}>; + +export type InsideFunctionSpecifics = { getBrowserInstance: GetBrowserInstance; forgetBrowserEventLoop: ForgetBrowserEventLoop; timer: DebuggingTimer; + generateRandomId: GenerateRenderId; + deleteTmpDir: () => Promise; + getCurrentFunctionName: () => string; + getCurrentMemorySizeInMb: () => number; +}; + +export type FullClientSpecifics = { + id: '__remotion_full_client_specifics'; + bundleSite: typeof bundle; + readDirectory: ReadDir; + uploadDir: UploadDir; + createFunction: CreateFunction; + checkCredentials: () => void; }; export type ProviderSpecifics = { @@ -254,11 +353,16 @@ export type ProviderSpecifics = { callFunctionAsync: CallFunctionAsync; callFunctionStreaming: CallFunctionStreaming; callFunctionSync: CallFunctionSync; - getCurrentFunctionName: () => string; estimatePrice: EstimatePrice; getLoggingUrlForRendererFunction: GetLoggingUrlForRendererFunction; getLoggingUrlForMethod: GetLoggingUrlForMethod; getEphemeralStorageForPriceCalculation: () => number; getOutputUrl: GetOutputUrl; isFlakyError: (err: Error) => boolean; + serverStorageProductName: () => string; + getMaxStillInlinePayloadSize: () => number; + getMaxNonInlinePayloadSizePerFunction: () => number; + getAccountId: GetAccountId; + deleteFunction: DeleteFunction; + getFunctions: GetFunctions; }; diff --git a/packages/serverless/src/render-progress.ts b/packages/serverless/src/render-progress.ts index 08853097bde..566881b5f98 100644 --- a/packages/serverless/src/render-progress.ts +++ b/packages/serverless/src/render-progress.ts @@ -6,7 +6,7 @@ import type { CostsInfo, ReceivedArtifact, } from './types'; -import type {EnhancedErrorInfo} from './write-lambda-error'; +import type {EnhancedErrorInfo} from './write-error-to-storage'; export type CleanupInfo = { doneIn: number | null; diff --git a/packages/serverless/src/streaming/streaming.ts b/packages/serverless/src/streaming/streaming.ts index bc0ea5ee04d..5cb15363968 100644 --- a/packages/serverless/src/streaming/streaming.ts +++ b/packages/serverless/src/streaming/streaming.ts @@ -1,7 +1,7 @@ import {makeStreamPayloadMessage} from '@remotion/streaming'; import type {SerializedArtifact} from '../serialize-artifact'; -import type {CloudProvider, RenderStillLambdaResponsePayload} from '../types'; -import type {LambdaErrorInfo} from '../write-lambda-error'; +import type {CloudProvider, RenderStillFunctionResponsePayload} from '../types'; +import type {FunctionErrorInfo} from '../write-error-to-storage'; const framesRendered = 'frames-rendered' as const; const errorOccurred = 'error-occurred' as const; @@ -10,7 +10,7 @@ const videoChunkRendered = 'video-chunk-rendered' as const; const audioChunkRendered = 'audio-chunk-rendered' as const; const chunkComplete = 'chunk-complete' as const; const stillRendered = 'still-rendered' as const; -const lambdaInvoked = 'lambda-invoked' as const; +const functionInvoked = 'lambda-invoked' as const; const artifactEmitted = 'artifact-emitted' as const; const messageTypes = { @@ -21,7 +21,7 @@ const messageTypes = { '5': {type: audioChunkRendered}, '6': {type: stillRendered}, '7': {type: chunkComplete}, - '8': {type: lambdaInvoked}, + '8': {type: functionInvoked}, '9': {type: artifactEmitted}, } as const; @@ -36,7 +36,7 @@ export const formatMap: {[key in MessageType]: 'json' | 'binary'} = { [audioChunkRendered]: 'binary', [stillRendered]: 'json', [chunkComplete]: 'json', - [lambdaInvoked]: 'json', + [functionInvoked]: 'json', [artifactEmitted]: 'json', }; @@ -61,7 +61,7 @@ export type StreamingPayload = payload: { error: string; shouldRetry: boolean; - errorInfo: LambdaErrorInfo; + errorInfo: FunctionErrorInfo; }; } | { @@ -72,7 +72,7 @@ export type StreamingPayload = } | { type: typeof stillRendered; - payload: RenderStillLambdaResponsePayload; + payload: RenderStillFunctionResponsePayload; } | { type: typeof chunkComplete; @@ -82,7 +82,7 @@ export type StreamingPayload = }; } | { - type: typeof lambdaInvoked; + type: typeof functionInvoked; payload: { attempt: number; }; diff --git a/packages/serverless/src/test/best-frames-per-function.test.ts b/packages/serverless/src/test/best-frames-per-function.test.ts index 60d96b23d10..77a2fa76707 100644 --- a/packages/serverless/src/test/best-frames-per-function.test.ts +++ b/packages/serverless/src/test/best-frames-per-function.test.ts @@ -1,7 +1,7 @@ import {expect, test} from 'bun:test'; import {bestFramesPerFunctionParam} from '../best-frames-per-function-param'; -test('Get reasonable framesPerLambda defaults', () => { +test('Get reasonable framesPerFunction defaults', () => { expect(bestFramesPerFunctionParam(20)).toEqual(20); expect(bestFramesPerFunctionParam(21)).toEqual(11); expect(bestFramesPerFunctionParam(100)).toEqual(20); diff --git a/packages/serverless/src/test/expected-out-name.test.ts b/packages/serverless/src/test/expected-out-name.test.ts index 43c9e3e9dfe..79edb89ec84 100644 --- a/packages/serverless/src/test/expected-out-name.test.ts +++ b/packages/serverless/src/test/expected-out-name.test.ts @@ -11,6 +11,7 @@ type MockProvider = { s3Key: string; s3Url: string; }; + creationFunctionOptions: {}; }; const testRenderMetadata: RenderMetadata = { diff --git a/packages/serverless/src/test/most-expensive.test.ts b/packages/serverless/src/test/most-expensive.test.ts index bf3ef7b3397..e07d583bf18 100644 --- a/packages/serverless/src/test/most-expensive.test.ts +++ b/packages/serverless/src/test/most-expensive.test.ts @@ -40,7 +40,7 @@ test('Should calculate most expensive chunks', () => { start: 2000, }, ], - framesPerLambda: 10, + framesPerFunction: 10, firstFrame: 0, lastFrame: 99, }); @@ -55,7 +55,7 @@ test('Should calculate most expensive chunks', () => { }); test('Render starting from frame 10 should have correct offset', () => { - const framesPerLambda = 10; + const framesPerFunction = 10; const firstFrame = 10; const lastFrame = 99; const most = getMostExpensiveChunks({ @@ -96,7 +96,7 @@ test('Render starting from frame 10 should have correct offset', () => { start: 2000, }, ], - framesPerLambda, + framesPerFunction, firstFrame, lastFrame, }); @@ -111,7 +111,7 @@ test('Render starting from frame 10 should have correct offset', () => { }); test('Render starting from frame 10 and last chunk in most expensive should be corret', () => { - const framesPerLambda = 10; + const framesPerFunction = 10; const firstFrame = 10; const lastFrame = 79; const most = getMostExpensiveChunks({ @@ -152,7 +152,7 @@ test('Render starting from frame 10 and last chunk in most expensive should be c start: 2000, }, ], - framesPerLambda, + framesPerFunction, firstFrame, lastFrame, }); diff --git a/packages/serverless/src/test/plan-frame-ranges.test.ts b/packages/serverless/src/test/plan-frame-ranges.test.ts index 831c96cbc07..0a93e372266 100644 --- a/packages/serverless/src/test/plan-frame-ranges.test.ts +++ b/packages/serverless/src/test/plan-frame-ranges.test.ts @@ -3,7 +3,7 @@ import {planFrameRanges} from '../plan-frame-ranges'; test('Plan frame ranges should respect everyNthFrame', () => { const planned = planFrameRanges({ - framesPerLambda: 8, + framesPerFunction: 8, everyNthFrame: 2, frameRange: [0, 99], }); @@ -20,7 +20,7 @@ test('Plan frame ranges should respect everyNthFrame', () => { test('Should remove ranges that are not going to render', () => { const planned = planFrameRanges({ - framesPerLambda: 11, + framesPerFunction: 11, everyNthFrame: 1, frameRange: [0, 22], }); @@ -33,7 +33,7 @@ test('Should remove ranges that are not going to render', () => { test('Should not have a bug that was reported', () => { const planned = planFrameRanges({ - framesPerLambda: 138, + framesPerFunction: 138, everyNthFrame: 1, frameRange: [15000, 35559], }); diff --git a/packages/serverless/src/types.ts b/packages/serverless/src/types.ts index eb32d098e14..3bb0be40b69 100644 --- a/packages/serverless/src/types.ts +++ b/packages/serverless/src/types.ts @@ -6,10 +6,15 @@ export interface CloudProvider< string, unknown >, + CreateFunctionSpecifics extends Record = Record< + string, + unknown + >, > { type: string; region: Region; receivedArtifactType: ReceivedArtifactType; + creationFunctionOptions: CreateFunctionSpecifics; } export type ReceivedArtifact = { @@ -26,17 +31,18 @@ export type CostsInfo = { disclaimer: string; }; -export type RenderStillLambdaResponsePayload = { - type: 'success'; - output: string; - outKey: string; - size: number; - bucketName: string; - sizeInBytes: number; - estimatedPrice: CostsInfo; - renderId: string; - receivedArtifacts: ReceivedArtifact[]; -}; +export type RenderStillFunctionResponsePayload = + { + type: 'success'; + output: string; + outKey: string; + size: number; + bucketName: string; + sizeInBytes: number; + estimatedPrice: CostsInfo; + renderId: string; + receivedArtifacts: ReceivedArtifact[]; + }; export type ChunkRetry = { chunk: number; diff --git a/packages/serverless/src/validate-composition.ts b/packages/serverless/src/validate-composition.ts index df9eb13ba65..16280b8fd25 100644 --- a/packages/serverless/src/validate-composition.ts +++ b/packages/serverless/src/validate-composition.ts @@ -10,6 +10,11 @@ import type {VideoConfig} from 'remotion/no-react'; import type {Await} from './await'; import type {ProviderSpecifics} from './provider-implementation'; import type {CloudProvider} from './types'; +import { + validateDimension, + validateDurationInFrames, + validateFps, +} from './validate'; type ValidateCompositionOptions = { serveUrl: string; @@ -68,9 +73,21 @@ export const validateComposition = async ({ onServeUrlVisited, }); - return { + const videoConfig: VideoConfig = { ...comp, height: forceHeight ?? comp.height, width: forceWidth ?? comp.width, }; + + const reason = `of the "" component with the id "${composition}"`; + + validateDurationInFrames(comp.durationInFrames, { + component: reason, + allowFloats: false, + }); + validateFps(comp.fps, reason, false); + validateDimension(comp.height, 'height', reason); + validateDimension(comp.width, 'width', reason); + + return videoConfig; }; diff --git a/packages/serverless/src/validate-frames-per-function.ts b/packages/serverless/src/validate-frames-per-function.ts index 6f34e6afe85..ecd9187a21a 100644 --- a/packages/serverless/src/validate-frames-per-function.ts +++ b/packages/serverless/src/validate-frames-per-function.ts @@ -1,43 +1,43 @@ import {MINIMUM_FRAMES_PER_LAMBDA} from './constants'; export const validateFramesPerFunction = ({ - framesPerLambda, + framesPerFunction, durationInFrames, }: { - framesPerLambda: unknown; + framesPerFunction: unknown; durationInFrames: number; }) => { - if (framesPerLambda === null) { + if (framesPerFunction === null) { return; } - if (framesPerLambda === undefined) { + if (framesPerFunction === undefined) { return; } - if (typeof framesPerLambda !== 'number') { + if (typeof framesPerFunction !== 'number') { throw new TypeError( `'framesPerLambda' needs to be a number, passed ${JSON.stringify( - framesPerLambda, + framesPerFunction, )}`, ); } - if (!Number.isFinite(framesPerLambda)) { + if (!Number.isFinite(framesPerFunction)) { throw new TypeError( - `'framesPerLambda' needs to be finite, passed ${framesPerLambda}`, + `'framesPerLambda' needs to be finite, passed ${framesPerFunction}`, ); } - if (Number.isNaN(framesPerLambda)) { + if (Number.isNaN(framesPerFunction)) { throw new TypeError( - `'framesPerLambda' needs to be NaN, passed ${framesPerLambda}`, + `'framesPerLambda' needs to be NaN, passed ${framesPerFunction}`, ); } - if (framesPerLambda % 1 !== 0) { + if (framesPerFunction % 1 !== 0) { throw new TypeError( - `'framesPerLambda' needs to be an integer, passed ${framesPerLambda}`, + `'framesPerLambda' needs to be an integer, passed ${framesPerFunction}`, ); } @@ -46,9 +46,9 @@ export const validateFramesPerFunction = ({ durationInFrames, ); - if (framesPerLambda < effectiveMinimum) { + if (framesPerFunction < effectiveMinimum) { throw new TypeError( - `The framesPerLambda needs to be at least ${effectiveMinimum}, but is ${framesPerLambda}`, + `The framesPerLambda needs to be at least ${effectiveMinimum}, but is ${framesPerFunction}`, ); } }; diff --git a/packages/serverless/src/write-lambda-error.ts b/packages/serverless/src/write-error-to-storage.ts similarity index 89% rename from packages/serverless/src/write-lambda-error.ts rename to packages/serverless/src/write-error-to-storage.ts index 0e39fb67415..ee64c102c78 100644 --- a/packages/serverless/src/write-lambda-error.ts +++ b/packages/serverless/src/write-error-to-storage.ts @@ -3,7 +3,7 @@ import type {FileNameAndSize} from './get-files-in-folder'; import type {ProviderSpecifics} from './provider-implementation'; import type {CloudProvider} from './types'; -export type LambdaErrorInfo = { +export type FunctionErrorInfo = { type: 'renderer' | 'browser' | 'stitcher' | 'webhook' | 'artifact'; message: string; name: string; @@ -20,7 +20,7 @@ export type LambdaErrorInfo = { export const getTmpDirStateIfENoSp = ( err: string, providerSpecifics: ProviderSpecifics, -): LambdaErrorInfo['tmpDir'] => { +): FunctionErrorInfo['tmpDir'] => { if (!errorIsOutOfSpaceError(err)) { return null; } @@ -36,7 +36,7 @@ export const getTmpDirStateIfENoSp = ( }; }; -export type EnhancedErrorInfo = LambdaErrorInfo & { +export type EnhancedErrorInfo = FunctionErrorInfo & { /** * @deprecated Will always be an empty string. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59ede27667e..62b771f2065 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1388,9 +1388,6 @@ importers: pureimage: specifier: 0.4.13 version: 0.4.13 - vitest: - specifier: 0.31.1 - version: 0.31.1(happy-dom@15.10.2)(jsdom@21.1.0) zip-lib: specifier: ^0.7.2 version: 0.7.2 @@ -1734,6 +1731,9 @@ importers: packages/serverless: dependencies: + '@remotion/bundler': + specifier: workspace:* + version: link:../bundler '@remotion/renderer': specifier: workspace:* version: link:../renderer