最近发现阿里云的一些服务都挺良心的,比如说Serverless相关的函数计算,居然有大量的免费额度,每月 40万/GB-秒。GB-秒这个单位的意思是一个 1GB 内存的实例主机运行 1 秒,而函数计算就是运行在这个实例中的,并且这个实例还是包含ffmpeg环境的。

假设我们利用这些免费额度处理音视频,设置运行实例的内存为3GB,如果平均每次处理耗时为30秒,那么可以处理4000多次。另外,函数计算除了主动调用以外还可以被阿里云的其它服务事件触发,例如上传文件到oss这个事件。

由于oss本身就支持强大的媒体处理能力,所以我们讨论一些其它ffmpeg的用法——视频渲染,例如将一些视频、图片、音乐资源合成一个新视频。

下面我们来讨论一个简单的方案:

  1. 将图片、音乐等资源和一个命令配置文件打包成zip资源压缩包。
  2. 将资源压缩包上传到oss,触发函数计算
  3. 函数计算从oss下载资源压缩包并解压,然后解析ffmpeg命令配置文件,执行相应ffmpeg命令
  4. 将最终的产物(一般是视频,也可能是一些其它东西)上传到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/

https://github.com/devsapp/fc

https://github.com/devsapp/start-fc

https://gist.github.com/protrolium/e0dbd4bb0f1a396fcb55