├── .gitignore ├── package.json ├── README.md ├── src ├── index.js ├── s3-util.js └── child-process-promise.js └── template.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | output.yml 4 | .vscode 5 | .idea 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s3-lambda-ffmpeg-mov-to-mp4-s3", 3 | "version": "1.0.0", 4 | "description": "A Serverless Application Repository Component for converting MOV files from one S3 bucket, into MP4 files to another S3 bucket.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "package": "aws cloudformation package --template-file template.yml --output-template-file output.yml --s3-bucket app-repo-components", 8 | "deploy": "aws cloudformation deploy --template-file output.yml --stack-name s3-lambda-ffmpeg-mov-to-mp4-s3 --capabilities CAPABILITY_IAM", 9 | "qd": "npm run package && npm run deploy" 10 | }, 11 | "keywords": [], 12 | "author": "Aleksandar Simovic ", 13 | "license": "MIT" 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # S3 Bucket -> Lambda (FFMPEG Convert MOV to MP4) -> S3 Bucket 3 | 4 | ## Description 5 | 6 | This is a serverless component that takes uploaded MOV video files from one S3 Bucket, then using FFMPEG Lambda Layer converts them to MP4 and uploads to another S3 Bucket. It contains: 7 | 8 | - an Input S3 Bucket that accepts MOV video files. 9 | 10 | - a Lambda that takes the MOV video file from the Input S3 bucket, converts it to a MP4 one and uploads it to the Output bucket 11 | 12 | - an Output S3 Bucket that receives MP4 files. 13 | 14 | ## Deployment Parameters 15 | 16 | This component has one CloudFormation deployment parameter: 17 | 18 | - `ConversionTimeout`, an optional parameter, represents the timeout of the Conversion Lambda function. By default it's 180 seconds. 19 | 20 | ## Latest Release - 1.0.0 21 | 22 | - Initial release. 23 | 24 | ## Roadmap - Upcoming changes 25 | 26 | Here are the upcoming changes that I'll add to this serverless component: 27 | 28 | - ESLint 29 | - Tests -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const s3Util = require('./s3-util'), 2 | childProcessPromise = require('./child-process-promise'), 3 | path = require('path'), 4 | os = require('os'), 5 | EXTENSION = process.env.EXTENSION, 6 | OUTPUT_BUCKET = process.env.OUTPUT_BUCKET, 7 | MIME_TYPE = process.env.MIME_TYPE; 8 | 9 | exports.handler = function (eventObject, context) { 10 | const eventRecord = eventObject.Records && eventObject.Records[0], 11 | inputBucket = eventRecord.s3.bucket.name, 12 | key = eventRecord.s3.object.key, 13 | id = context.awsRequestId, 14 | resultKey = key.replace(/\.[^.]+$/, EXTENSION), 15 | workdir = os.tmpdir(), 16 | inputFile = path.join(workdir, id + path.extname(key)), 17 | outputFile = path.join(workdir, 'converted-' + id + EXTENSION); 18 | 19 | 20 | console.log('converting', inputBucket, key, 'using', inputFile); 21 | return s3Util.downloadFileFromS3(inputBucket, key, inputFile) 22 | .then(() => childProcessPromise.spawn( 23 | '/opt/bin/ffmpeg', 24 | ['-loglevel', 'error', '-y', '-i', inputFile, '-f', 'mp4', outputFile], 25 | {env: process.env, cwd: workdir} 26 | )) 27 | .then(() => s3Util.uploadFileToS3(OUTPUT_BUCKET, resultKey, outputFile, MIME_TYPE)); 28 | }; -------------------------------------------------------------------------------- /src/s3-util.js: -------------------------------------------------------------------------------- 1 | /*global module, require, Promise, console */ 2 | 3 | const aws = require('aws-sdk'), 4 | fs = require('fs'), 5 | s3 = new aws.S3(), 6 | downloadFileFromS3 = function (bucket, fileKey, filePath) { 7 | 'use strict'; 8 | console.log('downloading', bucket, fileKey, filePath); 9 | return new Promise(function (resolve, reject) { 10 | const file = fs.createWriteStream(filePath), 11 | stream = s3.getObject({ 12 | Bucket: bucket, 13 | Key: fileKey 14 | }).createReadStream(); 15 | stream.on('error', reject); 16 | file.on('error', reject); 17 | file.on('finish', function () { 18 | console.log('downloaded', bucket, fileKey); 19 | resolve(filePath); 20 | }); 21 | stream.pipe(file); 22 | }); 23 | }, uploadFileToS3 = function (bucket, fileKey, filePath, contentType) { 24 | 'use strict'; 25 | console.log('uploading', bucket, fileKey, filePath); 26 | return s3.upload({ 27 | Bucket: bucket, 28 | Key: fileKey, 29 | Body: fs.createReadStream(filePath), 30 | ACL: 'private', 31 | ContentType: contentType 32 | }).promise(); 33 | }; 34 | 35 | module.exports = { 36 | downloadFileFromS3: downloadFileFromS3, 37 | uploadFileToS3: uploadFileToS3 38 | }; -------------------------------------------------------------------------------- /src/child-process-promise.js: -------------------------------------------------------------------------------- 1 | /*global module, require, console, Promise */ 2 | 'use strict'; 3 | const childProcess = require('child_process'), 4 | execPromise = function (command) { 5 | return new Promise((resolve, reject) => { 6 | childProcess.exec(command, (err, result) => { 7 | if (err) { 8 | reject(err); 9 | } else { 10 | resolve(result && String(result).trim()); 11 | } 12 | }); 13 | }); 14 | }, 15 | spawnPromise = function (command, argsarray, envOptions) { 16 | return new Promise((resolve, reject) => { 17 | console.log('executing', command, argsarray.join(' ')); 18 | const childProc = childProcess.spawn(command, argsarray, envOptions || {env: process.env, cwd: process.cwd()}), 19 | resultBuffers = []; 20 | childProc.stdout.on('data', buffer => { 21 | console.log(buffer.toString()); 22 | resultBuffers.push(buffer); 23 | }); 24 | childProc.stderr.on('data', buffer => console.error(buffer.toString())); 25 | childProc.on('exit', (code, signal) => { 26 | console.log(`${command} completed with ${code}:${signal}`); 27 | if (code !== 0) { 28 | reject(`${command} failed with ${code || signal}`); 29 | } else { 30 | resolve(Buffer.concat(resultBuffers).toString().trim()); 31 | } 32 | }); 33 | }); 34 | }; 35 | module.exports = { 36 | exec: execPromise, 37 | spawn: spawnPromise 38 | }; -------------------------------------------------------------------------------- /template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: A serverless component for conversion from MOV to MP4 4 | Parameters: 5 | ConversionTimeout: 6 | Type: Number 7 | Default: 180 8 | InputS3BucketName: 9 | Type: String 10 | Default: 's3-lambda-ffmpeg-input-mov-bucket' 11 | MinLength: 3 12 | OutputS3BucketName: 13 | Type: String 14 | Default: 's3-lambda-ffmpeg-output-mp4-bucket' 15 | MinLength: 3 16 | Resources: 17 | InputS3Bucket: 18 | Type: AWS::S3::Bucket 19 | Properties: 20 | BucketName: !Ref InputS3BucketName 21 | ConvertFileFunction: 22 | Type: AWS::Serverless::Function 23 | Properties: 24 | Handler: index.handler 25 | Timeout: !Ref ConversionTimeout 26 | MemorySize: 1024 27 | Runtime: nodejs8.10 28 | CodeUri: src/ 29 | Layers: 30 | - 'arn:aws:lambda:us-east-1:145266761615:layer:ffmpeg:4' 31 | Policies: 32 | - S3CrudPolicy: 33 | BucketName: !Ref OutputS3BucketName 34 | - S3ReadPolicy: 35 | BucketName: !Ref InputS3BucketName 36 | Environment: 37 | Variables: 38 | OUTPUT_BUCKET: !Ref OutputS3Bucket 39 | EXTENSION: '.mp4' 40 | MIME_TYPE: 'video/mp4' 41 | Events: 42 | FileUpload: 43 | Type: S3 44 | Properties: 45 | Bucket: !Ref InputS3Bucket 46 | Events: s3:ObjectCreated:* 47 | OutputS3Bucket: 48 | Type: AWS::S3::Bucket 49 | Properties: 50 | BucketName: !Ref OutputS3BucketName 51 | 52 | Outputs: 53 | InputS3Bucket: 54 | Description: Input S3 bucket 55 | Value: !Ref InputS3Bucket 56 | OutputS3Bucket: 57 | Description: Output S3 bucket 58 | Value: !Ref OutputS3Bucket 59 | 60 | --------------------------------------------------------------------------------