我就说阿里怎么可能这么良心:【产品变更】免费额度过渡方案
自2023年01月01日起,函数计算原有的免费额度(每月前100万次函数调用免费和每月前40万GB*秒的资源使用免费)正式取消。改为新用户赠送免费试用额度。
2022-12-23 Aitsuki
最近发现阿里云的一些服务都挺良心的,比如说Serverless相关的函数计算,居然有大量的免费额度,每月 40万/GB-秒。GB-秒
这个单位的意思是一个 1GB 内存的实例主机运行 1 秒,而函数计算就是运行在这个实例中的,并且这个实例还是包含ffmpeg环境的。
假设我们利用这些免费额度处理音视频,设置运行实例的内存为3GB,如果平均每次处理耗时为30秒,那么可以处理4000多次。另外,函数计算除了主动调用以外还可以被阿里云的其它服务事件触发,例如上传文件到oss这个事件。
由于oss本身就支持强大的媒体处理能力,所以我们讨论一些其它ffmpeg的用法——视频渲染,例如将一些视频、图片、音乐资源合成一个新视频。
下面我们来讨论一个简单的方案:
- 将图片、音乐等资源和一个命令配置文件打包成zip资源压缩包。
- 将资源压缩包上传到oss,触发函数计算
- 函数计算从oss下载资源压缩包并解压,然后解析ffmpeg命令配置文件,执行相应ffmpeg命令
- 将最终的产物(一般是视频,也可能是一些其它东西)上传到oss
其中,阿里云的入网流量和内网流量都是免费的,所以这个过程不会产生oss流量费用,但是oss保存文件是需要费用的,可以在oss上设置生命周期规则,定义合适的存储策略。
资源压缩包约定
首先我们需要将所有资源放入一个文件夹中,这个文件夹将作为函数计算的工作目录,所有命令将在这个目录中执行。工作目录的根目录中还需要放置一个命令配置文件,用来配置需要执行的命令,这个命令配置文件我们约定为 cmd.json
。
例如下面是一个名为test的资源目录,目录结构随意,只要保证根目录有cmd.json
test
├── butterfly.mp3
├── cmd.json
└── img
├── 1.jpg
├── 2.jpg
└── 3.jpg
命令配置文件格式约定
首先,我们将配置文件的命名约定为 cmd.json
,并放置在资源目录的根目录。然后是 cmd.json
的格式,为了灵活性我们可以做以下约定:
- “cmds”: 需要执行的命令,不局限于ffmpeg命令,只要是函数计算运行实例环境支持的即可(实例环境支持自定义配置,已内置ffmpeg)。
- “product”: ffmpeg命令执行完毕后的产物,可能是一个视频,或者是一个文件夹。就根而言,是ffmpeg的output。
- “uploadTo”:product上传到oss bucket的指定目录。
案例1:渲染视频
将上面资源目录中的内容制作成视频30秒的视频,每一秒切换一张图片,并添加bgm,最后上传到bucket的指定路径。
需要执行的ffmpeg命令:
- 裁剪音频 (指定区间)
- 将图片合成视频
- 并入音轨
{
"cmds": [
"ffmpeg -i butterfly.mp3 -ss 00:01:25 -t 30 tmp.mp3",
"ffmpeg -loop 1 -framerate 1 -i img/%d.jpg -t 30 -r 15 tmp.mp4",
"ffmpeg -i tmp.mp4 -i tmp.mp3 test.mp4"
],
"product": "test.mp4",
"uploadTo": "video"
}
案例2:视频截图并分离音频流和视频流
每隔5秒截图一次,并且分离音轨。
test
├── test.mp4
└── cmd.json
需要执行的ffmpeg命令
- 分离视频流 (如果不确定视频格式,需要去掉
-codec copy
参数) - 分离音频流 (如果不确定音频格式,需要去掉
-acodec copy
参数) - 视频截图(5秒一次)
{
"cmds": [
"mkdir output"
"ffmpeg -i test.mp4 -codec copy -an output/slient.mp4",
"ffmpeg -i test.mp4 -acodec copy -vn output/music.m4a",
"ffmpeg -i test.mp4 -vf fps=1/5 output/img%d.png"
],
"product": "output",
"uploadTo": "resource/test"
}
函数代码
import json
import logging
import os
import shutil
import subprocess
import oss2
def handler(event, context):
logger = logging.getLogger()
logger.info(event)
evt = json.loads(event)['events'][0]
bucket = get_bucket(evt, context)
# 下载并解压资源压缩包
archive_key = evt['oss']['object']['key']
archive = os.path.join('/tmp', archive_key)
unpackdir = os.path.dirname(archive)
if os.path.exists(unpackdir):
shutil.rmtree(unpackdir)
os.mkdir(unpackdir)
bucket.get_object_to_file(archive_key, archive)
shutil.unpack_archive(archive, unpackdir)
# 解析 cmd.json
workdir = archive.replace('.zip', '')
cmdobj = json.load(open(os.path.join(workdir, 'cmd.json'), 'r'))
scmds: str = cmdobj['cmds']
product: str = cmdobj['product']
uploadto: str = cmdobj['uploadTo']
# 执行命令
for scmd in scmds:
cmd = scmd.split(' ')
subprocess.run(cmd, cwd=workdir, check=True)
# 上传文件到阿里云
productfile = os.path.join(workdir, product)
if os.path.isfile(productfile):
bucket.put_object_from_file(uploadto + '/' + product, productfile)
elif os.path.isdir(productfile):
walk = os.walk(productfile)
for root, _, files in walk:
for file in files:
f = os.path.join(root, file)
bucket.put_object_from_file(
uploadto + '/' + os.path.relpath(f, productfile), f)
else:
raise Exception("product is not a file or directory: ", product)
return
def get_bucket(evt, context):
creds = context.credentials
# Required by OSS sdk
auth = oss2.StsAuth(
creds.access_key_id,
creds.access_key_secret,
creds.security_token)
bucket_name = evt['oss']['bucket']['name']
# 需要注意的是endpoint这里要用内网地址,否则会产生额外的oss流量费用
endpoint = 'oss-' + evt['region'] + '-internal.aliyuncs.com'
bucket = oss2.Bucket(auth, endpoint, bucket_name)
return bucket
部署
可以在函数计算的控制台页面手动操作:https://fcnext.console.aliyun.com/
或者使用Serverless devs工具部署: https://www.serverless-devs.com/
需要注意的是oss触发器事件的设置是这三个:
- oss:ObjectCreated:PutObject
- oss:ObjectCreated:PostObject
- oss:ObjectCreated:CompleteMultipartUpload
最后,我准备了上面两个案例的资源压缩包,可用于测试:
Referencies
https://www.serverless-devs.com/