├── .gitignore ├── make-aws-lambda-zip ├── .travis.yml ├── delete-aws-lambda ├── deploy-aws-lambda ├── launch-aws-lambda ├── flags ├── launch-lambda-example.js ├── lambda.js ├── create-aws-lambda ├── package.json ├── index.test.js ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | secrets 2 | node_modules 3 | out 4 | lambda.zip 5 | *.mp4 6 | package-lock.json 7 | .nyc_output/ 8 | -------------------------------------------------------------------------------- /make-aws-lambda-zip: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | zip \ 4 | --symlinks \ 5 | --exclude=*node_modules/aws-sdk* \ 6 | --recurse-paths lambda.zip \ 7 | lambda.js package.json node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "12" 5 | script: 6 | - npm t 7 | deploy: 8 | provider: npm 9 | email: crifei93@gmail.com 10 | api_key: $NPM_TOKEN 11 | on: 12 | tags: true 13 | -------------------------------------------------------------------------------- /delete-aws-lambda: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source flags 4 | 5 | echo "LAMBDA_NAME: $LAMBDA_NAME" 6 | echo "REGION: $REGION" 7 | 8 | aws lambda delete-function \ 9 | --region $REGION \ 10 | --function-name $LAMBDA_NAME -------------------------------------------------------------------------------- /deploy-aws-lambda: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source flags 4 | 5 | echo "LAMBDA_NAME: $LAMBDA_NAME" 6 | echo "REGION: $REGION" 7 | 8 | ./make-aws-lambda-zip 9 | 10 | aws lambda update-function-code \ 11 | --region $REGION \ 12 | --function-name $LAMBDA_NAME \ 13 | --zip-file "fileb://lambda.zip" 14 | -------------------------------------------------------------------------------- /launch-aws-lambda: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source flags 4 | 5 | echo "LAMBDA_NAME: $LAMBDA_NAME" 6 | echo "REGION: $REGION" 7 | 8 | aws lambda invoke --region $REGION --function-name $LAMBDA_NAME --payload "{\"bucket\":\"$BUCKET_NAME\", \"amount\": 48, \"fps\": 6}" out --log-type Tail --query 'LogResult' --output text | base64 --decode 9 | 10 | cat out 11 | -------------------------------------------------------------------------------- /flags: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while [ ! $# -eq 0 ] 4 | do 5 | case "$1" in 6 | --bucket | -b) 7 | BUCKET="$2" 8 | ;; 9 | --region) 10 | REGION="$2" 11 | ;; 12 | --lambda | -l) 13 | LAMBDA_NAME="$2" 14 | ;; 15 | --role) 16 | ROLE_ARN="$2" 17 | ;; 18 | --ffmpeg | -f) 19 | FFMPEG_LAYER_ARN="$2" 20 | ;; 21 | esac 22 | shift 23 | done 24 | -------------------------------------------------------------------------------- /launch-lambda-example.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const AWS = require('aws-sdk') 4 | 5 | const region = 'us-east-1' 6 | 7 | const apiVersion = 'latest' 8 | const lambda = new AWS.Lambda({ apiVersion, region }) 9 | const invokeParams = { FunctionName: 'GardenTimelapse' } 10 | 11 | lambda.invoke(invokeParams, (err, data) => { 12 | console.log(err, data) 13 | }) 14 | -------------------------------------------------------------------------------- /lambda.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const main = require('.') 4 | 5 | exports.handler = function ({ bucket = 'snapshots', name = 'timelapse.mp4', amount = 24, fps = 6 }, context, cb) { 6 | main.downloadS3Images({ bucket, amount }) 7 | .then(() => main.createTimelapse({ name, fps })) 8 | .then(() => main.saveTimelapseToS3({ bucket, name })) 9 | .then((data) => cb(null, { success: true, data, bucket, name, amount, fps })) 10 | .catch(err => cb(err)) 11 | } 12 | -------------------------------------------------------------------------------- /create-aws-lambda: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source flags 4 | 5 | echo "LAMBDA_NAME: $LAMBDA_NAME" 6 | echo "ROLE_ARN: $ROLE_ARN" 7 | echo "REGION: $REGION" 8 | echo "FFMPEG_LAYER_ARN: $FFMPEG_LAYER_ARN" 9 | 10 | aws lambda create-function \ 11 | --region $REGION \ 12 | --function-name $LAMBDA_NAME \ 13 | --runtime nodejs10.x \ 14 | --timeout 180 \ 15 | --memory 2048 \ 16 | --region $REGION \ 17 | --role $ROLE_ARN \ 18 | --handler lambda.handler \ 19 | --zip-file "fileb://lambda.zip" 20 | 21 | aws lambda update-function-configuration \ 22 | --region $REGION \ 23 | --function-name $LAMBDA_NAME \ 24 | --layers $FFMPEG_LAYER_ARN -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timelapse-lambda", 3 | "version": "1.0.0", 4 | "description": "make a timelapse from a set of snapshots with AWS Lambda and S3", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tap index.test.js", 8 | "delete-lambda": "./delete-aws-lamdba", 9 | "create-lambda": "./create-aws-lamdba", 10 | "deploy-lambda": "./make-aws-lambda-zip && ./deploy-aws-lambda", 11 | "launch-lambda": "./launch-aws-lambda" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "aws-sdk": "^2.518.0", 18 | "fluent-ffmpeg": "^2.1.2" 19 | }, 20 | "devDependencies": { 21 | "sinon": "^7.4.1", 22 | "tap": "^14.6.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const sinon = require('sinon') 3 | const { timelapsePathFor, downloadS3Images } = require('.') 4 | tap.pass('this is fine') 5 | 6 | tap.test('timelapsePathFor - get tmp path for filename', (t) => { 7 | t.plan(1) 8 | const timelapsePath = timelapsePathFor('timelapse.mp4') 9 | t.deepEqual('/tmp/timelapse.mp4', timelapsePath) 10 | }) 11 | 12 | tap.test('downloadS3Images - saves snapshots in bucket to disk', async (t) => { 13 | const listObjects = sinon.spy(() => Promise.resolve({ Contents: [{ Key: '2019-08-30T17:37:33.540Z.jpg' }] })) 14 | const getObject = sinon.spy(({ Bucket, Key }) => Promise.resolve({ Body: '...' })) 15 | const writeFile = sinon.spy((path, contents) => Promise.resolve()) 16 | 17 | await downloadS3Images({ bucket: 'snapshots', amount: 1 }, { listObjects, getObject, writeFile }) 18 | 19 | t.true(listObjects.calledWithExactly({ Bucket: 'snapshots' })) 20 | t.true(getObject.calledWithExactly({ Bucket: 'snapshots', Key: '2019-08-30T17:37:33.540Z.jpg' })) 21 | t.true(writeFile.calledWithExactly('/tmp/0.jpg', '...')) 22 | }) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # timelapse-lambda 2 | 3 | ## installation 4 | 5 | ### 1. Deploy the [ffmpeg-lambda-layer](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:145266761615:applications~ffmpeg-lambda-layer) on your Lambda 6 | 7 | ### 2. Create an S3 bucket 8 | 9 | name it, for example, "garden-snapshots" **in the same region as the ffmpeg-lambda-layer** 10 | 11 | ### 3. Create timelapse lambda 12 | 13 | run `./create-aws-lambda` by specifying the needed parameters, below you can find an example. 14 | 15 | **in the same region as above!** 16 | 17 | ``` 18 | ./create-aws-lambda \ 19 | --region us-east-1 \ 20 | --lambda Timelapse \ 21 | --role arn:aws:iam::XXXXXXXXXXX:role/lambda_name \ 22 | --ffmpeg arn:aws:lambda:us-east-1:XXXXXXXXXXX:layer:ffmpeg:1 23 | ``` 24 | 25 | ### 4. update the lambda with the code 26 | 27 | specify the region and the lambda name as above and run: 28 | 29 | ``` 30 | ./deploy-aws-lambda \ 31 | --region us-east-1 \ 32 | --lambda Timelapse 33 | ``` 34 | 35 | # usage 36 | 37 | `npm install` and take a look at `launch-lambda-example.js`: 38 | 39 | ```js 40 | #!/usr/bin/env node 41 | 42 | const AWS = require('aws-sdk') 43 | 44 | const region = 'us-east-1' 45 | 46 | const apiVersion = 'latest' 47 | const lambda = new AWS.Lambda({ apiVersion, region }) 48 | const invokeParams = { FunctionName: 'Timelapse' } 49 | 50 | lambda.invoke(invokeParams, (err, data) => { 51 | console.log(err, data) 52 | }) 53 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.timelapsePathFor = timelapsePathFor 2 | exports.downloadS3Images = downloadS3Images 3 | exports.createTimelapse = createTimelapse 4 | exports.saveTimelapseToS3 = saveTimelapseToS3 5 | 6 | const { promisify } = require('util') 7 | const fs = require('fs') 8 | const fluentffmpeg = require('fluent-ffmpeg') 9 | 10 | const AWS = require('aws-sdk') 11 | const S3 = new AWS.S3() 12 | 13 | function timelapsePathFor (name) { return `/tmp/${name}` } 14 | 15 | function downloadS3Images ({ bucket, amount } = {}, { listObjects = promisify(S3.listObjects).bind(S3), getObject = promisify(S3.getObject).bind(S3), writeFile = promisify(fs.writeFile).bind(fs) } = {}) { 16 | if (!bucket) throw new Error('bucket missing') 17 | return listObjects({ Bucket: bucket }) 18 | .then(data => { 19 | const tasks = data.Contents 20 | .filter(d => d.Key.startsWith('201')) 21 | .filter((d, i) => i > (data.Contents.length - 1 - amount)).map(d => d.Key) 22 | .map(file => getObject({ Bucket: bucket, Key: file })) 23 | return Promise.all(tasks) 24 | }) 25 | .then(files => { 26 | const tasks = files.map((file, i) => writeFile(`/tmp/${i}.jpg`, file.Body)) 27 | return Promise.all(tasks) 28 | }) 29 | } 30 | 31 | function createTimelapse ({ name, fps } = {}, { ffmpeg = fluentffmpeg } = {}) { 32 | const timelapsePath = timelapsePathFor(name) 33 | return new Promise((resolve, reject) => { 34 | ffmpeg().addInput('/tmp/%01d.jpg').noAudio().outputOptions(`-r ${fps}`).videoCodec('libx264') 35 | .on('error', (err) => { console.error('Error during processing', err); reject(err) }) 36 | .on('end', () => { console.log('Processing finished !'); resolve() }) 37 | .save(timelapsePath, { end: true }) 38 | }) 39 | } 40 | 41 | function saveTimelapseToS3 ({ bucket, name }, { putObject = promisify(S3.putObject).bind(S3) }) { 42 | const timelapsePath = timelapsePathFor(name) 43 | return putObject({ Body: fs.readFileSync(timelapsePath), Bucket: bucket, Key: name }) 44 | } 45 | --------------------------------------------------------------------------------