使用AWS Lambda构建基于ffmpeg的视频处理Serverless服务
背景
公司有一块业务需要对生成的视频做100倍快放处理。视频一般是6个小时。视频由某直播推流服务生成,保存在云端。对其进行快放处理使用了ffmpeg来操作,是一个计算密集型的操作,放在业务服务器上,容易在处理过程中造成业务服务器的性能下降。该业务处理并不是全天候的,一天大概十几例的样子,单独购买服务器也很不划算。以上是从成本考虑。
从该业务类型上考虑,该业务十分独立。给定原视频云存储地址,处理,保存到云存储上。和其他业务耦合性十分低。
结合以上两点,是现在的Serverless完美的适用场景。
下面将使用AWS的Lambda服务,结合AWS S3云存储来实现上述需求。
前置知识
- 有亚马逊账号
- 对AWS有一定了解
- 对云存储有一定了解
- 对JavaScript有一定了解
- 对ffmpeg有一定了解
注册账号
AWS官网:https://aws.amazon.com/cn/
这是全球站,可以注册个人账号。
AWS在国内也有网站:https://www.amazonaws.cn
但是这里注册不了个人账号,只能注册企业账号。
在https://aws.zmazon.com/cn 注册账号的时候按照网站提示一步一步来就好了。最后会要绑定外币信用卡。
官方默认提供了很多免费服务。常规的主机,最低配置免费12个月,其他很多服务也在一定使用量情况下是免费的。本文中提到的Lambda服务和S3服务也都是在一定使用量情况下免费。
设置语言
我们使用的是全球站,虽然进入 https://aws.amazon.com/cn ,显示的都是中文,但是用里面的服务的时候,这些页面很多都是默认英文展示的。所以首先要设置语言为中文。
然后出现下面的页面
点击本地化和默认区域
这一栏的编辑
按钮,出现下面页面,选择中文(简体)
即可。保存设置。
以后访问的AWS页面,会默认提供中文页面。
设置区域
AWS有很多区域,我们在使用不同的服务的时候,尽量都在同一个区域内。一般都使用美国东部-弗吉尼亚北部
这个区域。
设置S3云存储
S3是AWS的云存储服务。可以用来存储各种类型的资源。在当前文章中,会用来存储视频。
创建Bucket(桶)
在当前案例中,只需要填写桶的名称就好了,其他配置想都是默认,注意区域,后面创建Lambda
服务的时候也需要在同一个区域。点击最后的创建存储桶
。
点击进入该桶的详情页面。
下面就是在该桶下创建了video文件夹,并在该文件夹内上传了一个视频文件
到这里S3云存储的设置和使用就大致了解了。
设置Lambda
进入Lambda页面后,注意 区域 必须和刚刚创建的S3 存储桶 在一个区域。点击创建函数。
新建好的函数如下:
示例代码是一段js,当前的基础环境就是nodejs-16,我们可以理解为当前这个所谓的函数
是由一个Linux系统中的nodejs来执行的一段js代码。
这个函数的执行要有一个触发点,所以等会还要设置触发器。触发器就是S3服务中的相关桶的文件新增。
除了nodejs环境还不够,我们还要有ffmpeg环境,但是AWS提供的基础环境中没有。基础环境一般有nodejs、python、ruby、java等。在这里,ffmpeg并不是要我们去在一个Linux里面安装,这是Serverless服务,没有服务器的概念。
ffmpeg作为在基础环境中的,第二个层环境,AWS有固定的规范来进行设置。在这里我们可以在AWS提供的第三方的代码库中找到ffmpeg环境的层。
在这儿,我们找到了别人维护的ffmpeg的环境,可以和Lambda的基础环境兼容。也就是说可以作为一层加到 基础的环境上,组成一个新的环境。既有nodejs,又有ffmpeg。该作者还提供了实例代码,使用nodejs来调用系统的命令,执行ffmpeg相关的命令。查看源代码:https://github.com/serverlesspub/ffmpeg-aws-lambda-layer。该代码中存在一些小的错误。后面使用的代码经过修改。
我们进入这个兼容环境的详情页后,设置一个名称,就可以部署到自己的 层
里面使用了。
下面是创建成功的页面,不用做任务动作
我们回到Lambda主页,点击层
会发现,我们刚刚 使用别人 的部署 好的 层。
点击进入,复制 该 层 的识别字符串
然后回到我们刚刚新建的 函数页面,拉到最下面。
最后点击 添加。
修改基础代码
图中的代码是按https://github.com/serverlesspub/ffmpeg-aws-lambda-layer 代码调整的。
- 修改几处错误
- 修改ffmpeg的处理命令
index.js
const s3Util = require('./s3-util'),
childProcessPromise = require('./child-process-promise'),
path = require('path'),
os = require('os'),
EXTENSION = process.env.EXTENSION,
THUMB_WIDTH = process.env.THUMB_WIDTH,
OUTPUT_BUCKET = process.env.OUTPUT_BUCKET,
MIME_TYPE = process.env.MIME_TYPE;
exports.handler = function (eventObject, context) {
const eventRecord = eventObject.Records && eventObject.Records[0],
inputBucket = eventRecord.s3.bucket.name,
key = eventRecord.s3.object.key,
id = context.awsRequestId,
resultKey = key.replace(/\.[^.]+$/, EXTENSION),
workdir = os.tmpdir(),
inputFile = path.join(workdir, id + path.extname(key)),
outputFile = path.join(workdir, id + '-output.' + EXTENSION);
console.log('converting', inputBucket, key, 'using', inputFile);
return s3Util.downloadFileFromS3(inputBucket, key, inputFile)
.then(() => childProcessPromise.spawn(
'/opt/bin/ffmpeg',
['-i', inputFile, ' -filter_complex ', ` '[0:v]setpts=0.04*PTS[v]' `, ' -map ', ` '[v]' `, outputFile],
{env: process.env, cwd: workdir}
))
.then(() => s3Util.uploadFileToS3(OUTPUT_BUCKET, resultKey, outputFile, MIME_TYPE));
};
s3-util.js
/*global module, require, Promise, console */
const aws = require('aws-sdk'),
fs = require('fs'),
s3 = new aws.S3(),
downloadFileFromS3 = function (bucket, fileKey, filePath) {
'use strict';
console.log('downloading', bucket, fileKey, filePath);
return new Promise(function (resolve, reject) {
const file = fs.createWriteStream(filePath),
stream = s3.getObject({
Bucket: bucket,
Key: fileKey
}).createReadStream();
stream.on('error', reject);
file.on('error', reject);
file.on('finish', function () {
console.log('downloaded', bucket, fileKey);
resolve(filePath);
});
stream.pipe(file);
});
}, uploadFileToS3 = function (bucket, fileKey, filePath, contentType) {
'use strict';
console.log('uploading', bucket, fileKey, filePath);
return s3.upload({
Bucket: bucket,
Key: fileKey,
Body: fs.createReadStream(filePath),
ACL: 'private',
ContentType: contentType
}).promise();
};
module.exports = {
downloadFileFromS3: downloadFileFromS3,
uploadFileToS3: uploadFileToS3
};
child-process-promise.js
/*global module, require, console, Promise */
'use strict';
const childProcess = require('child_process'),
spawnPromise = function (command, argsarray, envOptions) {
return new Promise((resolve, reject) => {
console.log('executing', command, argsarray.join(' '));
const childProc = childProcess.spawn(command, argsarray, envOptions || {env: process.env, cwd: process.cwd()}),
resultBuffers = [];
childProc.stdout.on('data', buffer => {
console.log(buffer.toString());
resultBuffers.push(buffer);
});
childProc.stderr.on('data', buffer => console.error(buffer.toString()));
childProc.on('exit', (code, signal) => {
console.log(`${command} completed with ${code}:${signal}`);
if (code || signal) {
reject(`${command} failed with ${code || signal}`);
} else {
resolve(Buffer.concat(resultBuffers).toString().trim());
}
});
});
};
module.exports = {
spawn: spawnPromise
};
添加触发器
这儿,为了保险起见,我们还需再见一个桶,用来存储处理后的文件。避免可能的递归调用风险。
配置权限
点击 最后 的 附加策略 即可。
配置环境变量
函数在执行的时候,可以通过一些注入环境变量的方式,给这个函数以更多的灵活性。在上述js代码中,会从环境变量中获取
- 生成文件的后缀:EXTENSION
- 生成文件所要保存的桶:OUTPUT_BUCKET
- 生成文件的MIME值:MIME_TYPE
配置内存使用和超时时间
AWS的Lambda执行默认有时间限制 以及 内存使用限制。我们当前业务下,这些都太小了。内存应该改为2048M,超时设置为10分钟。这些需要根据具体业务,反复的调试来确定的。
向S3上传mp4视频,触发执行函数处理视频
以下使生成的倍速视频文件。