├── .github
└── workflows
│ └── registry-publish.yml
├── .gitignore
├── ffmpeg-app
├── .gitignore
├── hook
│ └── index.js
├── publish.yaml
├── readme.md
├── src
│ ├── functions
│ │ ├── audio_convert
│ │ │ └── index.py
│ │ ├── get_duration
│ │ │ └── index.py
│ │ ├── get_multimedia_meta
│ │ │ └── index.py
│ │ ├── get_sprites
│ │ │ └── index.py
│ │ ├── video_gif
│ │ │ └── index.py
│ │ └── video_watermark
│ │ │ ├── index.py
│ │ │ ├── logo.gif
│ │ │ └── logo.png
│ ├── readme.md
│ └── s.yaml
└── version.md
├── headless-ffmpeg
├── .gitignore
├── hook
│ └── index.js
├── publish.yaml
├── readme.md
├── src
│ ├── .gitignore
│ ├── code
│ │ ├── Dockerfile
│ │ ├── record.js
│ │ ├── record.sh
│ │ └── server.js
│ ├── dest
│ │ ├── fail
│ │ │ └── index.py
│ │ └── succ
│ │ │ └── index.py
│ ├── readme.md
│ └── s.yaml
└── version.md
├── http-transcode
├── .gitignore
├── hook
│ └── index.js
├── publish.yaml
├── readme.md
├── src
│ ├── code
│ │ ├── fail
│ │ │ └── index.py
│ │ ├── succ
│ │ │ └── index.py
│ │ └── transcode
│ │ │ └── index.py
│ ├── readme.md
│ └── s.yaml
└── version.md
├── publish.py
├── readme.md
├── rtmp-snapshot
├── .gitignore
├── hook
│ └── index.js
├── publish.yaml
├── readme.md
├── src
│ ├── code
│ │ ├── fail
│ │ │ └── index.py
│ │ ├── snapshot
│ │ │ └── index.py
│ │ └── succ
│ │ │ └── index.py
│ ├── readme.md
│ └── s.yaml
└── version.md
├── serverless-ffmpeg-online
├── .gitignore
├── hook
│ └── index.js
├── publish.yaml
├── readme.md
├── src
│ ├── .gitignore
│ ├── code
│ │ ├── Dockerfile
│ │ └── control.js
│ ├── dest
│ │ ├── fail
│ │ │ └── index.py
│ │ └── succ
│ │ │ └── index.py
│ ├── readme.md
│ └── s.yaml
└── version.md
├── serverless-panoramic-page-recording-http
├── .gitignore
├── hook
│ └── index.js
├── publish.yaml
├── readme.md
├── src
│ ├── .gitignore
│ ├── code
│ │ ├── Dockerfile
│ │ ├── record.js
│ │ ├── record.sh
│ │ └── server.js
│ ├── dest
│ │ ├── fail
│ │ │ └── index.py
│ │ └── succ
│ │ │ └── index.py
│ ├── readme.md
│ └── s.yaml
└── version.md
├── transcode
├── .gitignore
├── hook
│ └── index.js
├── publish.yaml
├── readme.md
├── src
│ ├── code
│ │ ├── fail
│ │ │ └── index.py
│ │ ├── succ
│ │ │ └── index.py
│ │ └── transcode
│ │ │ └── index.py
│ ├── readme.md
│ └── s.yaml
└── version.md
├── update.list
└── video-process-flow
├── .gitignore
├── hook
└── index.js
├── publish.yaml
├── readme.md
├── src
├── code
│ ├── after-process
│ │ └── index.py
│ ├── flows
│ │ ├── input-fc.json
│ │ └── video-processing-fc.yml
│ ├── merge
│ │ └── index.py
│ ├── oss-trigger
│ │ └── index.py
│ ├── split
│ │ └── index.py
│ └── transcode
│ │ └── index.py
├── readme.md
└── s.yaml
└── version.md
/.github/workflows/registry-publish.yml:
--------------------------------------------------------------------------------
1 | name: publish package to serverless-hub
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Set up Python
13 | uses: actions/setup-python@v2
14 | with:
15 | python-version: '3.x'
16 | - uses: actions/setup-node@v1
17 | with:
18 | node-version: 12
19 | - name: Install dependencies
20 | run: |
21 | python -m pip install --upgrade pip
22 | pip install setuptools wheel twine
23 | pip install requests
24 | - name: Publish package
25 | env:
26 | publish_token: ${{ secrets.alibaba_registry_publish_token }}
27 | run: |
28 | ls
29 | python publish.py
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 |
132 | .idea
133 | .DS_Store
134 | c
135 | .s
136 | node_modules
137 |
--------------------------------------------------------------------------------
/ffmpeg-app/.gitignore:
--------------------------------------------------------------------------------
1 | .fun
2 | .s
--------------------------------------------------------------------------------
/ffmpeg-app/hook/index.js:
--------------------------------------------------------------------------------
1 | async function preInit(inputObj) {
2 | console.log(`\n _______ _______ __ __ _______ _______ _______
3 | | || || |_| || || || |
4 | | ___|| ___|| || _ || ___|| ___|
5 | | |___ | |___ | || |_| || |___ | | __
6 | | ___|| ___|| || ___|| ___|| || |
7 | | | | | | ||_|| || | | |___ | |_| |
8 | |___| |___| |_| |_||___| |_______||_______|
9 | `)
10 | }
11 |
12 | async function postInit(inputObj) {
13 | console.log(`\n Welcome to the ffmpeg-app application
14 | This application requires to open these services:
15 | FC : https://fc.console.aliyun.com/
16 | OSS: https://oss.console.aliyun.com/
17 |
18 | * 关于项目的介绍,可以参考:https://github.com/devsapp/start-ffmpeg/tree/master/ffmpeg-app/src
19 | * 项目初始化完成,您可以直接进入项目目录下,并使用 s deploy 进行项目部署\n`)
20 | }
21 |
22 | module.exports = {
23 | postInit,
24 | preInit
25 | }
26 |
--------------------------------------------------------------------------------
/ffmpeg-app/publish.yaml:
--------------------------------------------------------------------------------
1 | Type: Application
2 | Name: ffmpeg-app
3 | Version: 0.1.1
4 | Provider:
5 | - 阿里云
6 | Description: 基于FFmpeg的音视频处理应用, 包括获取音视频元信息、获取音视频时长、音频转换、雪碧图生成、生成 GIF、打水印等多个模块。
7 | HomePage: https://github.com/devsapp/start-ffmpeg/tree/master/ffmpeg-app
8 | Tags:
9 | - ffmpeg
10 | - 音视频
11 | - 转码
12 | - 音频
13 | - 视频
14 | Category: 音视频处理
15 | Service:
16 | 函数计算:
17 | Authorities:
18 | - AliyunFCFullAccess
19 | Parameters:
20 | type: object
21 | additionalProperties: false # 不允许增加其他属性
22 | required: # 必填项
23 | - region
24 | - serviceName
25 | - roleArn
26 | properties:
27 | region:
28 | title: 地域
29 | type: string
30 | default: cn-hangzhou
31 | description: 创建应用所在的地区
32 | enum:
33 | - cn-beijing
34 | - cn-hangzhou
35 | - cn-shanghai
36 | - cn-qingdao
37 | - cn-zhangjiakou
38 | - cn-huhehaote
39 | - cn-shenzhen
40 | - cn-chengdu
41 | - cn-hongkong
42 | - ap-southeast-1
43 | - ap-southeast-2
44 | - ap-southeast-3
45 | - ap-southeast-5
46 | - ap-northeast-1
47 | - eu-central-1
48 | - eu-west-1
49 | - us-west-1
50 | - us-east-1
51 | - ap-south-1
52 | serviceName:
53 | title: 服务名
54 | type: string
55 | default: FcOssFFmpeg-${default-suffix}
56 | pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$"
57 | description: 应用所属的函数计算服务
58 | required: true
59 | roleArn:
60 | title: RAM角色ARN
61 | type: string
62 | default: ''
63 | pattern: '^acs:ram::[0-9]*:role/.*$'
64 | description: "函数计算访问其他云服务时使用的服务角色,需要填写具体的角色ARN,格式为acs:ram::$account-id>:role/$role-name。例如:acs:ram::14310000000:role/aliyunfcdefaultrole。
65 | \n如果您没有特殊要求,可以使用函数计算提供的默认的服务角色,即AliyunFCDefaultRole, 并增加 AliyunOSSFullAccess 权限。如果您首次使用函数计算,可以访问 https://fcnext.console.aliyun.com 进行授权。
66 | \n详细文档参考 https://help.aliyun.com/document_detail/181589.html#section-o93-dbr-z6o"
67 | required: true
68 | x-role:
69 | name: fcffmpegrole
70 | service: fc
71 | authorities:
72 | - AliyunOSSFullAccess
73 | - AliyunFCDefaultRolePolicy
74 |
--------------------------------------------------------------------------------
/ffmpeg-app/src/functions/audio_convert/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import subprocess
3 | import oss2
4 | import logging
5 | import json
6 | import os
7 | import time
8 |
9 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
10 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
11 |
12 | LOGGER = logging.getLogger()
13 |
14 | '''
15 | 1. function and bucket locate in same region
16 | 2. service's role has OSSFullAccess
17 | 3. event format
18 | {
19 | "bucket_name" : "test-bucket",
20 | "object_key" : "a.mp3",
21 | "output_dir" : "output/",
22 | "dst_type": ".wav",
23 | "ac": 1,
24 | "ar": 4000
25 | }
26 | '''
27 |
28 | # a decorator for print the excute time of a function
29 |
30 |
31 | def print_excute_time(func):
32 | def wrapper(*args, **kwargs):
33 | local_time = time.time()
34 | ret = func(*args, **kwargs)
35 | LOGGER.info('current Function [%s] excute time is %.2f seconds' %
36 | (func.__name__, time.time() - local_time))
37 | return ret
38 | return wrapper
39 |
40 |
41 | def get_fileNameExt(filename):
42 | (fileDir, tempfilename) = os.path.split(filename)
43 | (shortname, extension) = os.path.splitext(tempfilename)
44 | return fileDir, shortname, extension
45 |
46 |
47 | @print_excute_time
48 | def handler(event, context):
49 | LOGGER.info(event)
50 | evt = json.loads(event)
51 | oss_bucket_name = evt["bucket_name"]
52 | object_key = evt["object_key"]
53 | output_dir = evt["output_dir"]
54 | dst_type = evt["dst_type"]
55 | ac = evt.get("ac")
56 | ar = evt.get("ar")
57 |
58 | creds = context.credentials
59 | auth = oss2.StsAuth(creds.accessKeyId,
60 | creds.accessKeySecret, creds.securityToken)
61 | oss_client = oss2.Bucket(
62 | auth, 'oss-%s-internal.aliyuncs.com' % context.region, oss_bucket_name)
63 |
64 | exist = oss_client.object_exists(object_key)
65 | if not exist:
66 | raise Exception("object {} is not exist".format(object_key))
67 |
68 | input_path = oss_client.sign_url('GET', object_key, 3600)
69 | fileDir, shortname, extension = get_fileNameExt(object_key)
70 |
71 | cmd = ['ffmpeg', '-i', input_path,
72 | '/tmp/{0}{1}'.format(shortname, dst_type)]
73 | if ac:
74 | if ar:
75 | cmd = ['ffmpeg', '-i', input_path, "-ac",
76 | str(ac), "-ar", str(ar), '/tmp/{0}{1}'.format(shortname, dst_type)]
77 | else:
78 | cmd = ['ffmpeg', '-i', input_path, "-ac",
79 | str(ac), '/tmp/{0}{1}'.format(shortname, dst_type)]
80 | else:
81 | if ar:
82 | cmd = ['ffmpeg', '-i', input_path, "-ar",
83 | str(ar), '/tmp/{0}{1}'.format(shortname, dst_type)]
84 |
85 | LOGGER.info("cmd = {}".format(" ".join(cmd)))
86 | try:
87 | subprocess.run(
88 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
89 | except subprocess.CalledProcessError as exc:
90 | LOGGER.error('returncode:{}'.format(exc.returncode))
91 | LOGGER.error('cmd:{}'.format(exc.cmd))
92 | LOGGER.error('output:{}'.format(exc.output))
93 | LOGGER.error('stderr:{}'.format(exc.stderr))
94 | LOGGER.error('stdout:{}'.format(exc.stdout))
95 |
96 | for filename in os.listdir('/tmp/'):
97 | filepath = '/tmp/' + filename
98 | if filename.startswith(shortname):
99 | filekey = os.path.join(output_dir, fileDir, filename)
100 | oss_client.put_object_from_file(filekey, filepath)
101 | os.remove(filepath)
102 | LOGGER.info("Uploaded {} to {}".format(filepath, filekey))
103 | return "ok"
104 |
--------------------------------------------------------------------------------
/ffmpeg-app/src/functions/get_duration/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import subprocess
3 | import oss2
4 | import logging
5 | import json
6 | import os
7 | import time
8 |
9 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
10 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
11 |
12 | LOGGER = logging.getLogger()
13 |
14 | '''
15 | 1. function and bucket locate in same region
16 | 2. service's role has OSSReadAccess
17 | 3. event format
18 | {
19 | "bucket_name" : "test-bucket",
20 | "object_key" : "a.mp4"
21 | }
22 | '''
23 |
24 | # a decorator for print the excute time of a function
25 |
26 |
27 | def print_excute_time(func):
28 | def wrapper(*args, **kwargs):
29 | local_time = time.time()
30 | ret = func(*args, **kwargs)
31 | LOGGER.info('current Function [%s] excute time is %.2f seconds' %
32 | (func.__name__, time.time() - local_time))
33 | return ret
34 | return wrapper
35 |
36 |
37 | @print_excute_time
38 | def handler(event, context):
39 | evt = json.loads(event)
40 | oss_bucket_name = evt["bucket_name"]
41 | object_key = evt["object_key"]
42 | creds = context.credentials
43 | auth = oss2.StsAuth(creds.accessKeyId,
44 | creds.accessKeySecret, creds.securityToken)
45 | oss_client = oss2.Bucket(
46 | auth, 'oss-%s-internal.aliyuncs.com' % context.region, oss_bucket_name)
47 |
48 | exist = oss_client.object_exists(object_key)
49 | if not exist:
50 | raise Exception("object {} is not exist".format(object_key))
51 |
52 | object_url = oss_client.sign_url('GET', object_key, 15 * 60)
53 |
54 | cmd = ["ffprobe", "-show_entries", "format=duration",
55 | "-v", "quiet", "-of", "csv", "-i", object_url]
56 | raw_result = subprocess.check_output(cmd)
57 | result = raw_result.decode().replace("\n", "").strip().split(",")[1]
58 | duration = float(result)
59 | return duration
60 |
--------------------------------------------------------------------------------
/ffmpeg-app/src/functions/get_multimedia_meta/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import subprocess
3 | import oss2
4 | import logging
5 | import json
6 | import os
7 | import time
8 |
9 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
10 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
11 |
12 | LOGGER = logging.getLogger()
13 |
14 | '''
15 | 1. function and bucket locate in same region
16 | 2. service's role has OSSReadAccess
17 | 3. event format
18 | {
19 | "bucket_name" : "test-bucket",
20 | "object_key" : "a.mp4"
21 | }
22 | '''
23 |
24 | # a decorator for print the excute time of a function
25 |
26 |
27 | def print_excute_time(func):
28 | def wrapper(*args, **kwargs):
29 | local_time = time.time()
30 | ret = func(*args, **kwargs)
31 | LOGGER.info('current Function [%s] excute time is %.2f seconds' %
32 | (func.__name__, time.time() - local_time))
33 | return ret
34 | return wrapper
35 |
36 |
37 | @print_excute_time
38 | def handler(event, context):
39 | evt = json.loads(event)
40 | oss_bucket_name = evt["bucket_name"]
41 | object_key = evt["object_key"]
42 | creds = context.credentials
43 | auth = oss2.StsAuth(creds.accessKeyId,
44 | creds.accessKeySecret, creds.securityToken)
45 | oss_client = oss2.Bucket(
46 | auth, 'oss-%s-internal.aliyuncs.com' % context.region, oss_bucket_name)
47 |
48 | exist = oss_client.object_exists(object_key)
49 | if not exist:
50 | raise Exception("object {} is not exist".format(object_key))
51 |
52 | object_url = oss_client.sign_url('GET', object_key, 15 * 60)
53 |
54 | raw_result = subprocess.check_output(["ffprobe", "-v", "quiet", "-show_format", "-show_streams",
55 | "-print_format", "json", "-i", object_url])
56 | result = json.loads(raw_result)
57 |
58 | return result
59 |
--------------------------------------------------------------------------------
/ffmpeg-app/src/functions/get_sprites/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import subprocess
3 | import oss2
4 | import logging
5 | import json
6 | import os
7 | import time
8 |
9 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
10 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
11 |
12 | LOGGER = logging.getLogger()
13 |
14 | '''
15 | 1. function and bucket locate in same region
16 | 2. service's role has OSSFullAccess
17 | 3. event format
18 | {
19 | "bucket_name" : "test-bucket",
20 | "object_key" : "a.mp4",
21 | "output_dir" : "output/",
22 | "tile": "3*4",
23 | "start": 0,
24 | "duration": 10,
25 | "itsoffset": 0,
26 | "scale": "-1:-1",
27 | "interval": 2,
28 | "padding": 1,
29 | "color": "black",
30 | "dst_type": "jpg"
31 | }
32 | tile: 必填, 雪碧图的 rows * cols
33 | start: 可选, 默认是为 0
34 | duration: 可选,表示基于 start 之后的多长时间的视频内进行截图,
35 | 比如 start 为 10, duration 为 20,表示基于视频的10s-30s内进行截图
36 | interval: 可选,每隔多少秒截图一次, 默认为 1
37 | scale: 可选,截图的大小, 默认为 -1:-1, 默认为原视频大小, 320:240, iw/2:ih/2
38 | itsoffset: 可选,默认为 0, delay多少秒,配合start、interval使用
39 | - 假设 start 为 0, interval 为 10,itsoffset 为 0, 那么截图的秒数为 5, 15, 25 ...
40 | - 假设 start 为 0, interval 为 10,itsoffset 为 1, 那么截图的秒数为 4, 14, 24 ...
41 | - 假设 start 为 0, interval 为 10,itsoffset 为 4.999(不要写成5,不然会丢失0秒的那一帧图), 那么截图的秒数为 0, 10, 20 ...
42 | - 假设 start 为 0, interval 为 10,itsoffset 为 -1, 那么截图的秒数为 6, 16,26 ...
43 | padding: 可选,图片之间的间隔, 默认为 0
44 | color: 可选,雪碧图背景颜色,默认黑色, https://ffmpeg.org/ffmpeg-utils.html#color-syntax
45 | dst_type: 可选,生成的雪碧图图片格式,默认为 jpg,主要为 jpg 或者 png, https://ffmpeg.org/ffmpeg-all.html#image2-1
46 | '''
47 |
48 | # a decorator for print the excute time of a function
49 |
50 |
51 | def print_excute_time(func):
52 | def wrapper(*args, **kwargs):
53 | local_time = time.time()
54 | ret = func(*args, **kwargs)
55 | LOGGER.info('current Function [%s] excute time is %.2f seconds' %
56 | (func.__name__, time.time() - local_time))
57 | return ret
58 | return wrapper
59 |
60 |
61 | def get_fileNameExt(filename):
62 | (fileDir, tempfilename) = os.path.split(filename)
63 | (shortname, extension) = os.path.splitext(tempfilename)
64 | return fileDir, shortname, extension
65 |
66 |
67 | @print_excute_time
68 | def handler(event, context):
69 | LOGGER.info(event)
70 | evt = json.loads(event)
71 | oss_bucket_name = evt["bucket_name"]
72 | object_key = evt["object_key"]
73 | output_dir = evt["output_dir"]
74 | tile = evt["tile"]
75 | ss = evt.get("start", 0)
76 | ss = str(ss)
77 | t = evt.get("duration")
78 | if t:
79 | t = str(t)
80 |
81 | itsoffset = evt.get("itsoffset", 0)
82 | itsoffset = str(itsoffset)
83 | scale = evt.get("scale", "-1:-1")
84 | interval = str(evt.get("interval", 1))
85 | padding = str(evt.get("padding", 0))
86 | color = str(evt.get("color", "black"))
87 | dst_type = str(evt.get("dst_type", "jpg"))
88 |
89 | creds = context.credentials
90 | auth = oss2.StsAuth(creds.accessKeyId,
91 | creds.accessKeySecret, creds.securityToken)
92 | oss_client = oss2.Bucket(
93 | auth, 'oss-%s-internal.aliyuncs.com' % context.region, oss_bucket_name)
94 |
95 | exist = oss_client.object_exists(object_key)
96 | if not exist:
97 | raise Exception("object {} is not exist".format(object_key))
98 |
99 | input_path = oss_client.sign_url('GET', object_key, 3600)
100 | fileDir, shortname, extension = get_fileNameExt(object_key)
101 |
102 | cmd = ['ffmpeg', '-ss', ss, '-itsoffset', itsoffset, '-y', '-i', input_path,
103 | '-f', 'image2', '-vf', "fps=1/{0},scale={1},tile={2}:padding={3}:color={4}".format(
104 | interval, scale, tile, padding, color),
105 | '/tmp/{0}%d.{1}'.format(shortname, dst_type)]
106 |
107 | if t:
108 | cmd = ['ffmpeg', '-ss', ss, '-itsoffset', itsoffset, '-t', t, '-y', '-i', input_path,
109 | '-f', 'image2', '-vf', "fps=1/{0},scale={1},tile={2}:padding={3}:color={4}".format(
110 | interval, scale, tile, padding, color),
111 | '/tmp/{0}%d.{1}'.format(shortname, dst_type)]
112 |
113 | LOGGER.info("cmd = {}".format(" ".join(cmd)))
114 | try:
115 | subprocess.run(
116 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
117 | except subprocess.CalledProcessError as exc:
118 | LOGGER.error('returncode:{}'.format(exc.returncode))
119 | LOGGER.error('cmd:{}'.format(exc.cmd))
120 | LOGGER.error('output:{}'.format(exc.output))
121 | LOGGER.error('stderr:{}'.format(exc.stderr))
122 | LOGGER.error('stdout:{}'.format(exc.stdout))
123 |
124 | for filename in os.listdir('/tmp/'):
125 | filepath = '/tmp/' + filename
126 | if filename.startswith(shortname):
127 | filekey = os.path.join(output_dir, fileDir, filename)
128 | oss_client.put_object_from_file(filekey, filepath)
129 | os.remove(filepath)
130 | LOGGER.info("Uploaded {} to {}".format(filepath, filekey))
131 | return "ok"
132 |
--------------------------------------------------------------------------------
/ffmpeg-app/src/functions/video_gif/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import subprocess
3 | import oss2
4 | import logging
5 | import json
6 | import os
7 | import time
8 |
9 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
10 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
11 |
12 | LOGGER = logging.getLogger()
13 |
14 | '''
15 | 1. function and bucket locate in same region
16 | 2. service's role has OSSFullAccess
17 | 3. event format
18 | {
19 | "bucket_name" : "test-bucket",
20 | "object_key" : "a.mp4",
21 | "output_dir" : "output/",
22 | "vframes" : 20,
23 | "start": 0,
24 | "duration": 2
25 | }
26 | start 可选, 默认是为 0
27 | vframes 和 duration 可选, 当同时填写的时候, 以 duration 为准
28 | 当都没有填写的时候, 默认整个视频转为gif
29 | '''
30 |
31 | # a decorator for print the excute time of a function
32 |
33 |
34 | def print_excute_time(func):
35 | def wrapper(*args, **kwargs):
36 | local_time = time.time()
37 | ret = func(*args, **kwargs)
38 | LOGGER.info('current Function [%s] excute time is %.2f seconds' %
39 | (func.__name__, time.time() - local_time))
40 | return ret
41 | return wrapper
42 |
43 |
44 | def get_fileNameExt(filename):
45 | (fileDir, tempfilename) = os.path.split(filename)
46 | (shortname, extension) = os.path.splitext(tempfilename)
47 | return fileDir, shortname, extension
48 |
49 |
50 | @print_excute_time
51 | def handler(event, context):
52 | LOGGER.info(event)
53 | evt = json.loads(event)
54 | oss_bucket_name = evt["bucket_name"]
55 | object_key = evt["object_key"]
56 | output_dir = evt["output_dir"]
57 | vframes = evt.get("vframes")
58 | if vframes:
59 | vframes = str(vframes)
60 | ss = evt.get("start", 0)
61 | ss = str(ss)
62 | t = evt.get("duration")
63 | if t:
64 | t = str(t)
65 | creds = context.credentials
66 | auth = oss2.StsAuth(creds.accessKeyId,
67 | creds.accessKeySecret, creds.securityToken)
68 | oss_client = oss2.Bucket(
69 | auth, 'oss-%s-internal.aliyuncs.com' % context.region, oss_bucket_name)
70 |
71 | exist = oss_client.object_exists(object_key)
72 | if not exist:
73 | raise Exception("object {} is not exist".format(object_key))
74 |
75 | input_path = oss_client.sign_url('GET', object_key, 3600)
76 | fileDir, shortname, extension = get_fileNameExt(object_key)
77 | gif_path = os.path.join("/tmp", shortname + ".gif")
78 |
79 | cmd = ["ffmpeg", "-y", "-ss", ss, "-accurate_seek",
80 | "-i", input_path, "-pix_fmt", "rgb24", gif_path]
81 | if t:
82 | cmd = ["ffmpeg", "-y", "-ss", ss, "-t", t, "-accurate_seek",
83 | "-i", input_path, "-pix_fmt", "rgb24", gif_path]
84 | else:
85 | if vframes:
86 | cmd = ["ffmpeg", "-y", "-ss", ss, "-accurate_seek", "-i",
87 | input_path, "-vframes", vframes, "-y", "-f", "gif", gif_path]
88 |
89 | LOGGER.info("cmd = {}".format(" ".join(cmd)))
90 | try:
91 | subprocess.run(
92 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
93 | except subprocess.CalledProcessError as exc:
94 | LOGGER.error('returncode:{}'.format(exc.returncode))
95 | LOGGER.error('cmd:{}'.format(exc.cmd))
96 | LOGGER.error('output:{}'.format(exc.output))
97 | LOGGER.error('stderr:{}'.format(exc.stderr))
98 | LOGGER.error('stdout:{}'.format(exc.stdout))
99 |
100 | gif_key = os.path.join(output_dir, fileDir, shortname + ".gif")
101 |
102 | oss_client.put_object_from_file(gif_key, gif_path)
103 |
104 | LOGGER.info("Uploaded {} to {} ".format(
105 | gif_path, gif_key))
106 |
107 | os.remove(gif_path)
108 |
109 | return "ok"
110 |
--------------------------------------------------------------------------------
/ffmpeg-app/src/functions/video_watermark/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import subprocess
3 | import oss2
4 | import logging
5 | import json
6 | import os
7 | import time
8 |
9 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
10 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
11 |
12 | LOGGER = logging.getLogger()
13 |
14 | '''
15 | 1. function and bucket locate in same region
16 | 2. service's role has OSSFullAccess
17 | 3. event format
18 | {
19 | "bucket_name" : "test-bucket",
20 | "object_key" : "a.mp4",
21 | "output_dir" : "output/",
22 | "vf_args" : "drawtext=fontfile=/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc:text='hello函数计算':x=100:y=50:fontsize=24:fontcolor=red:shadowy=2",
23 | "filter_complex_args": "overlay=0:0:1"
24 | }
25 |
26 | filter_complex_args 优先级 > vf_args
27 |
28 | vf_args:
29 | - 文字水印
30 | vf_args = "drawtext=fontfile=/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc:text='hello函数计算':x=50:y=50:fontsize=24:fontcolor=red:shadowy=1"
31 | - 图片水印, 静态图片
32 | vf_args = "movie=/code/logo.png[watermark];[in][watermark]overlay=10:10[out]"
33 |
34 | filter_complex_args: 图片水印, 动态图片gif
35 | filter_complex_args = "overlay=0:0:1"
36 | '''
37 |
38 | # a decorator for print the excute time of a function
39 |
40 |
41 | def print_excute_time(func):
42 | def wrapper(*args, **kwargs):
43 | local_time = time.time()
44 | ret = func(*args, **kwargs)
45 | LOGGER.info('current Function [%s] excute time is %.2f seconds' %
46 | (func.__name__, time.time() - local_time))
47 | return ret
48 | return wrapper
49 |
50 |
51 | def get_fileNameExt(filename):
52 | (fileDir, tempfilename) = os.path.split(filename)
53 | (shortname, extension) = os.path.splitext(tempfilename)
54 | return fileDir, shortname, extension
55 |
56 |
57 | @print_excute_time
58 | def handler(event, context):
59 | LOGGER.info(event)
60 | evt = json.loads(event)
61 | oss_bucket_name = evt["bucket_name"]
62 | object_key = evt["object_key"]
63 | output_dir = evt["output_dir"]
64 | vf_args = evt.get("vf_args", "")
65 | filter_complex_args = evt.get("filter_complex_args")
66 |
67 | if not (vf_args or filter_complex_args):
68 | assert "at least one of 'vf_args' and 'filter_complex_args' has value"
69 |
70 | creds = context.credentials
71 | auth = oss2.StsAuth(creds.accessKeyId,
72 | creds.accessKeySecret, creds.securityToken)
73 | oss_client = oss2.Bucket(
74 | auth, 'oss-%s-internal.aliyuncs.com' % context.region, oss_bucket_name)
75 |
76 | exist = oss_client.object_exists(object_key)
77 | if not exist:
78 | raise Exception("object {} is not exist".format(object_key))
79 |
80 | input_path = oss_client.sign_url('GET', object_key, 3600)
81 | fileDir, shortname, extension = get_fileNameExt(object_key)
82 | dst_video_path = os.path.join("/tmp", "watermark_" + shortname + extension)
83 |
84 | cmd = ["ffmpeg", "-y", "-i", input_path,
85 | "-vf", vf_args, dst_video_path]
86 |
87 | if filter_complex_args: # gif
88 | cmd = ["ffmpeg", "-y", "-i", input_path, "-ignore_loop", "0",
89 | "-i", "/code/logo.gif", "-filter_complex", filter_complex_args, dst_video_path]
90 |
91 | LOGGER.info("cmd = {}".format(" ".join(cmd)))
92 | try:
93 | subprocess.run(
94 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
95 | except subprocess.CalledProcessError as exc:
96 | LOGGER.error('returncode:{}'.format(exc.returncode))
97 | LOGGER.error('cmd:{}'.format(exc.cmd))
98 | LOGGER.error('output:{}'.format(exc.output))
99 | LOGGER.error('stderr:{}'.format(exc.stderr))
100 | LOGGER.error('stdout:{}'.format(exc.stdout))
101 |
102 | video_key = os.path.join(output_dir, fileDir, shortname + extension)
103 | oss_client.put_object_from_file(video_key, dst_video_path)
104 |
105 | LOGGER.info("Uploaded {} to {} ".format(dst_video_path, video_key))
106 |
107 | os.remove(dst_video_path)
108 |
109 | return "ok"
110 |
--------------------------------------------------------------------------------
/ffmpeg-app/src/functions/video_watermark/logo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devsapp/start-ffmpeg/ccbf03699eb823d53e1ffebb336f8cfe363d41a7/ffmpeg-app/src/functions/video_watermark/logo.gif
--------------------------------------------------------------------------------
/ffmpeg-app/src/functions/video_watermark/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devsapp/start-ffmpeg/ccbf03699eb823d53e1ffebb336f8cfe363d41a7/ffmpeg-app/src/functions/video_watermark/logo.png
--------------------------------------------------------------------------------
/ffmpeg-app/src/s.yaml:
--------------------------------------------------------------------------------
1 | # ------------------------------------
2 | # 欢迎您使用阿里云函数计算 FC 组件进行项目开发
3 | # 组件仓库地址:https://github.com/devsapp/fc
4 | # 组件帮助文档:https://www.serverless-devs.com/fc/readme
5 | # Yaml参考文档:https://www.serverless-devs.com/fc/yaml/readme
6 | # 关于:
7 | # - Serverless Devs和FC组件的关系、如何声明/部署多个函数、超过50M的代码包如何部署
8 | # - 关于.fcignore使用方法、工具中.s目录是做什么、函数进行build操作之后如何处理build的产物
9 | # 等问题,可以参考文档:https://www.serverless-devs.com/fc/tips
10 | # 关于如何做CICD等问题,可以参考:https://www.serverless-devs.com/serverless-devs/cicd
11 | # 关于如何进行环境划分等问题,可以参考:https://www.serverless-devs.com/serverless-devs/extend
12 | # 更多函数计算案例,可参考:https://github.com/devsapp/awesome/
13 | # 有问题快来钉钉群问一下吧:33947367
14 | # ------------------------------------
15 | edition: 1.0.0 # 命令行YAML规范版本,遵循语义化版本(Semantic Versioning)规范
16 | name: ffmpeg-app # 项目名称
17 | # access 是当前应用所需要的密钥信息配置:
18 | # 密钥配置可以参考:https://www.serverless-devs.com/serverless-devs/command/config
19 | # 密钥使用顺序可以参考:https://www.serverless-devs.com/serverless-devs/tool#密钥使用顺序与规范
20 | access: "{{ access }}"
21 |
22 |
23 | vars:
24 | region: "{{ region }}"
25 | service:
26 | name: "{{ serviceName }}"
27 | description: Scenarios that can be solved by OSS + FC
28 | internetAccess: true
29 | role: "{{ roleArn }}"
30 | runtime: python3
31 | timeout: 900
32 | handler: index.handler
33 |
34 | services:
35 | AudioConvert: # 业务名称/模块名称
36 | component: fc # 组件名称,Serverless Devs 工具本身类似于一种游戏机,不具备具体的业务能力,组件类似于游戏卡,用户通过向游戏机中插入不同的游戏卡实现不同的功能,即通过使用不同的组件实现不同的具体业务能力
37 | # actions: # 自定义执行逻辑,关于actions 的使用,可以参考:https://www.serverless-devs.com/serverless-devs/yaml#行为描述
38 | # pre-deploy: # 在deploy之前运行
39 | # - run: s version publish -a demo
40 | # path: ./src
41 | # - run: docker build xxx # 要执行的系统命令,类似于一种钩子的形式
42 | # path: ./src # 执行系统命令/钩子的路径
43 | # - plugin: myplugin # 与运行的插件 (可以通过s cli registry search --type Plugin 获取组件列表)
44 | # args: # 插件的参数信息
45 | # testKey: testValue
46 | props: # 组件的属性值
47 | region: ${vars.region}
48 | service: ${vars.service}
49 | function:
50 | name: AudioConvert
51 | description: 音频格式转换器
52 | runtime: ${vars.runtime}
53 | codeUri: ./functions/audio_convert
54 | handler: ${vars.handler}
55 | memorySize: 1024
56 | timeout: ${vars.timeout}
57 |
58 | GetMediaMeta: # 服务名称
59 | component: fc
60 | props: # 组件的属性值
61 | region: ${vars.region}
62 | service: ${vars.service}
63 | function:
64 | name: GetMediaMeta
65 | description: 获取音视频 meta
66 | runtime: ${vars.runtime}
67 | codeUri: ./functions/get_multimedia_meta
68 | handler: ${vars.handler}
69 | memorySize: 1024
70 | timeout: ${vars.timeout}
71 |
72 | GetDuration: # 服务名称
73 | component: fc
74 | props: # 组件的属性值
75 | region: ${vars.region}
76 | service: ${vars.service}
77 | function:
78 | name: GetDuration
79 | description: 获取音视频时长
80 | runtime: ${vars.runtime}
81 | codeUri: ./functions/get_duration
82 | handler: ${vars.handler}
83 | memorySize: 1024
84 | timeout: ${vars.timeout}
85 |
86 | VideoGif: # 服务名称
87 | component: fc
88 | props: # 组件的属性值
89 | region: ${vars.region}
90 | service: ${vars.service}
91 | function:
92 | name: VideoGif
93 | description: 功能强大的 video 提取为 gif 函数
94 | runtime: ${vars.runtime}
95 | codeUri: ./functions/video_gif
96 | handler: ${vars.handler}
97 | memorySize: 1024
98 | timeout: ${vars.timeout}
99 |
100 | GetSprites: # 服务名称
101 | component: fc
102 | props: # 组件的属性值
103 | region: ${vars.region}
104 | service: ${vars.service}
105 | function:
106 | name: GetSprites
107 | description: 功能强大雪碧图制作函数
108 | runtime: ${vars.runtime}
109 | codeUri: ./functions/get_sprites
110 | handler: ${vars.handler}
111 | memorySize: 1024
112 | timeout: ${vars.timeout}
113 |
114 | VideoWatermark: # 服务名称
115 | component: fc
116 | props: # 组件的属性值
117 | region: ${vars.region}
118 | service: ${vars.service}
119 | function:
120 | name: VideoWatermark
121 | description: 功能强大的视频添加水印功能
122 | runtime: ${vars.runtime}
123 | codeUri: ./functions/video_watermark
124 | handler: ${vars.handler}
125 | memorySize: 1024
126 | timeout: ${vars.timeout}
127 |
128 | # next-function: # 第二个函数的案例,仅供参考
129 | # # 如果在当前项目下执行 s deploy,会同时部署模块:
130 | # # helloworld:服务hello-world-service,函数cpp-event-function
131 | # # next-function:服务hello-world-service,函数next-function-example
132 | # # 如果想单独部署当前服务与函数,可以执行 s + 模块名/业务名 + deploy,例如:s next-function deploy
133 | # # 如果想单独部署当前函数,可以执行 s + 模块名/业务名 + deploy function,例如:s next-function deploy function
134 | # # 更多命令可参考:https://www.serverless-devs.com/fc/readme#文档相关
135 | # component: fc
136 | # props:
137 | # region: ${vars.region}
138 | # service: ${vars.service} # 应用整体的服务配置
139 | # function: # 定义一个新的函数
140 | # name: next-function-example
141 | # description: 'hello world by serverless devs'
--------------------------------------------------------------------------------
/ffmpeg-app/version.md:
--------------------------------------------------------------------------------
1 | - 新版本支持
--------------------------------------------------------------------------------
/headless-ffmpeg/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | .s
3 |
--------------------------------------------------------------------------------
/headless-ffmpeg/hook/index.js:
--------------------------------------------------------------------------------
1 | async function preInit(inputObj) {
2 | console.log(`\n _______ _______ __ __ _______ _______ _______
3 | | || || |_| || || || |
4 | | ___|| ___|| || _ || ___|| ___|
5 | | |___ | |___ | || |_| || |___ | | __
6 | | ___|| ___|| || ___|| ___|| || |
7 | | | | | | ||_|| || | | |___ | |_| |
8 | |___| |___| |_| |_||___| |_______||_______|
9 | `)
10 | }
11 |
12 | async function postInit(inputObj) {
13 | console.log(`\n Welcome to the ffmpeg-app application
14 | This application requires to open these services:
15 | FC : https://fc.console.aliyun.com/
16 | OSS: https://oss.console.aliyun.com/
17 | ACR: https://cr.console.aliyun.com/
18 |
19 | * 关于项目的介绍,可以参考:https://github.com/devsapp/start-ffmpeg/blob/master/headless-ffmpeg/src
20 | * 项目初始化完成,您可以直接进入项目目录下
21 | 1. 对s.yaml进行升级,例如填充好environmentVariables中的部分变量值(OSS存储桶相关信息)
22 | 2. 开通容器镜像服务,并创建相关的实例、命名空间,并将内容对应填写到image字段中
23 | 3. 进行构建:s build --use-docker --dockerfile ./code/Dockerfile
24 | 4. 项目部署:s deploy --use-local -y
25 | * 最后您还可以验证项目的正确性,例如通过invoke调用(这里video_url等,可以考虑换成自己的测试mp4):
26 | s invoke -e '{"record_time":"35","video_url":"https://dy-vedio.oss-cn-hangzhou.aliyuncs.com/video/a.mp4","output_file":"record/test.mp4"}'
27 | \n`)
28 | }
29 |
30 | module.exports = {
31 | postInit,
32 | preInit
33 | }
34 |
--------------------------------------------------------------------------------
/headless-ffmpeg/publish.yaml:
--------------------------------------------------------------------------------
1 | Type: Application
2 | Name: headless-ffmpeg
3 | Provider:
4 | - 阿里云
5 | Version: 0.1.24
6 | Description: 快速部署一个全景录制的应用到阿里云函数计算
7 | HomePage: https://github.com/devsapp/start-ffmpeg/tree/master/headless-ffmpeg
8 | Tags:
9 | - 全景录制
10 | Category: 音视频处理
11 | Service:
12 | 函数计算:
13 | Authorities:
14 | - AliyunFCFullAccess
15 | - AliyunContainerRegistryFullAccess
16 | Parameters:
17 | type: object
18 | additionalProperties: false # 不允许增加其他属性
19 | required: # 必填项
20 | - region
21 | - serviceName
22 | - functionName
23 | - roleArn
24 | - acrImage
25 | - ossBucket
26 | - ossAkID
27 | - ossAkSecret
28 | - timeZone
29 | properties:
30 | region:
31 | title: 地域
32 | type: string
33 | default: cn-hangzhou
34 | description: 创建应用所在的地区
35 | enum:
36 | - cn-beijing
37 | - cn-hangzhou
38 | - cn-shanghai
39 | - cn-qingdao
40 | - cn-zhangjiakou
41 | - cn-huhehaote
42 | - cn-shenzhen
43 | - cn-chengdu
44 | - cn-hongkong
45 | - ap-southeast-1
46 | - ap-southeast-2
47 | - ap-southeast-3
48 | - ap-southeast-5
49 | - ap-northeast-1
50 | - eu-central-1
51 | - eu-west-1
52 | - us-west-1
53 | - us-east-1
54 | - ap-south-1
55 | serviceName:
56 | title: 服务名
57 | type: string
58 | default: browser_video_recorder-${default-suffix}
59 | pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$"
60 | description: 服务名称,只能包含字母、数字、下划线和中划线。不能以数字、中划线开头。长度在 1-128 之间
61 | functionName:
62 | title: 函数名
63 | type: string
64 | default: recoder
65 | description: 函数名称,只能包含字母、数字、下划线和中划线。不能以数字、中划线开头。长度在 1-64 之间
66 | roleArn:
67 | title: RAM角色ARN
68 | type: string
69 | default: ""
70 | pattern: "^acs:ram::[0-9]*:role/.*$"
71 | description: "函数计算访问其他云服务时使用的服务角色,需要填写具体的角色ARN,格式为acs:ram::$account-id>:role/$role-name。例如:acs:ram::14310000000:role/aliyunfcdefaultrole。
72 | \n如果您没有特殊要求,可以使用函数计算提供的默认的服务角色,即AliyunFCDefaultRole, 并增加 AliyunContainerRegistryFullAccess 权限。如果您首次使用函数计算,可以访问 https://fcnext.console.aliyun.com 进行授权。
73 | \n详细文档参考 https://help.aliyun.com/document_detail/181589.html#section-o93-dbr-z6o"
74 | required: true
75 | x-role:
76 | name: fcacrrole
77 | service: fc
78 | authorities:
79 | - AliyunContainerRegistryFullAccess
80 | - AliyunFCDefaultRolePolicy
81 | acrRegistry:
82 | title: 阿里云容器镜像
83 | type: string
84 | examples: ["registry.cn-hangzhou.aliyuncs.com/fc-demo/headless-ffmpeg:v1"]
85 | description: 阿里云容器镜像服务 image 的名字
86 | x-acr:
87 | type: "select"
88 | ossBucket:
89 | title: OSS 存储桶名
90 | type: string
91 | default: ""
92 | description: OSS 存储桶名
93 | x-bucket:
94 | dependency:
95 | - region
96 | ossAkID:
97 | title: OSS AK ID
98 | type: secret
99 | default: ""
100 | description: 能操作上面 OSS 存储桶的 ak id
101 | ossAkSecret:
102 | title: OSS AK Secret
103 | type: secret
104 | default: ""
105 | description: 能操作上面 OSS 存储桶的 ak secret
106 | timeZone:
107 | title: 时区
108 | type: string
109 | default: Asia/Shanghai
110 | description: 创建的应用函数执行时候所在实例的时区, 详情参考 https://docs.oracle.com/middleware/12211/wcs/tag-ref/MISC/TimeZones.html
111 |
--------------------------------------------------------------------------------
/headless-ffmpeg/readme.md:
--------------------------------------------------------------------------------
1 | src/readme.md
--------------------------------------------------------------------------------
/headless-ffmpeg/src/.gitignore:
--------------------------------------------------------------------------------
1 | /headless-ffmpeg
2 |
--------------------------------------------------------------------------------
/headless-ffmpeg/src/code/Dockerfile:
--------------------------------------------------------------------------------
1 | # https://hub.docker.com/r/aliyunfc/headless-ffmpeg
2 | FROM aliyunfc/headless-ffmpeg
3 |
4 | # set time zone (current is Shanghai, China)
5 | ENV TZ=Asia/Shanghai
6 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
7 | RUN dpkg-reconfigure -f noninteractive tzdata
8 |
9 | ENV LANG zh_CN.UTF-8
10 | ENV LANGUAGE zh_CN:zh
11 | ENV LC_ALL zh_CN.UTF-8
12 | # set Xvfb auth file
13 | ENV XAUTHORITY=/tmp/Xauthority
14 |
15 | WORKDIR /code
16 |
17 | ENV PUPPETEER_SKIP_DOWNLOAD=true
18 | RUN npm install puppeteer-core \
19 | express \
20 | ali-oss \
21 | --registry http://registry.npm.taobao.org
22 |
23 | COPY ./record.sh ./record.sh
24 | COPY ./record.js ./record.js
25 | COPY ./server.js ./server.js
26 |
27 | RUN mkdir -p /var/output
28 |
29 | EXPOSE 9000
30 |
31 | ENTRYPOINT ["node", "server.js"]
--------------------------------------------------------------------------------
/headless-ffmpeg/src/code/record.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer-core');
2 |
3 | var args = process.argv.splice(2)
4 | console.log(args);
5 |
6 | const scale_factor = parseFloat(args[3],10)
7 | const li = args[2].split(',');
8 | console.log(li)
9 | const w = parseInt(li[0], 10)
10 | const h = parseInt(li[1], 10)
11 |
12 | async function record(){
13 |
14 | var win_size = `${Math.floor(w/scale_factor)},${Math.floor(h/scale_factor)}`
15 | const browser = await puppeteer.launch(
16 | {
17 | headless: false,
18 | executablePath: "/usr/bin/google-chrome-stable",
19 | args: [
20 | '--no-sandbox',
21 | '--autoplay-policy=no-user-gesture-required',
22 | '--enable-usermedia-screen-capturing',
23 | '--allow-http-screen-capture',
24 | '--disable-gpu',
25 | '--start-fullscreen',
26 | '--window-size='+win_size,
27 | '--force-device-scale-factor=' + `${scale_factor}`
28 | ],
29 | ignoreDefaultArgs: ['--mute-audio', '--enable-automation']
30 | });
31 | console.log("try new page .....");
32 | const page = await browser.newPage();
33 |
34 | await page.setViewport({
35 | width: Math.floor(w/scale_factor),
36 | height: Math.floor(h/scale_factor),
37 | deviceScaleFactor: scale_factor
38 | });
39 | console.log("try goto .....");
40 | url = args[1] || "http://dy-vedio.oss-cn-hangzhou.aliyuncs.com/video/a.mp4";
41 | await page.goto(url);
42 | var timeout = parseInt(args[0], 10)*1000;
43 | console.log("waitFor begin .....");
44 | // const session = await page.target().createCDPSession();
45 | // await session.send('Emulation.setPageScaleFactor', {
46 | // pageScaleFactor: 0.75, // 75%
47 | // });
48 | await page.waitForTimeout(timeout);
49 | // console.log("screenshot .....");
50 | // await page.screenshot({ path: '/var/output/test.png' });
51 | await browser.close();
52 | console.log("browser closed ...........");
53 | }
54 |
55 | record();
--------------------------------------------------------------------------------
/headless-ffmpeg/src/code/record.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | #set -v
4 |
5 | wait_ready(){
6 | echo "wait until $1 success ..."
7 | for i in {1..30}
8 | do
9 | count=`ps -ef | grep $1 | grep -v "grep" | wc -l`
10 | if [ $count -gt 0 ]; then
11 | echo "$1 is ready!"
12 | break
13 | else
14 | sleep 1
15 | fi
16 | done
17 | }
18 |
19 | kill_pid () {
20 | local pids=`ps aux | grep $1 | grep -v grep | awk '{print $2}'`
21 | if [ "$pids" != "" ]; then
22 | echo "Killing the following $1 processes: $pids"
23 | kill -n $2 $pids
24 | else
25 | echo "No $1 processes to kill"
26 | fi
27 | }
28 |
29 | wait_shutdown(){
30 | echo "wait until $1 shutdown ..."
31 | for i in {1..30}
32 | do
33 | count=`ps -ef | grep $1 | grep -v "grep" | wc -l`
34 | if [ $count -eq 0 ]; then
35 | echo "$1 is shutdown!"
36 | break
37 | else
38 | sleep 1
39 | fi
40 | done
41 | }
42 |
43 | # start xvfb screen
44 | record_time=$1
45 | buff=300
46 | (( node_time_out=record_time+buff ))
47 | echo "start xvfb-run ..."
48 | xvfb-run --listen-tcp --server-num=76 --server-arg="-screen 0 $3" --auth-file=$XAUTHORITY nohup node record.js $node_time_out $2 $4 $6 > /tmp/chrome.log 2>&1 &
49 |
50 | # start pulseaudio service
51 | pulseaudio -D --exit-idle-time=-1
52 | pacmd load-module module-virtual-sink sink_name=v1
53 | pacmd set-default-sink v1
54 | pacmd set-default-source v1.monitor
55 |
56 | wait_ready pulseaudio
57 | wait_ready xvfb-run
58 | wait_ready Xvfb
59 | wait_ready chrome
60 | wait_ready record.js
61 |
62 | sleep 20s
63 |
64 | echo "ffmpeg start recording ..."
65 | nohup ffmpeg -y -loglevel debug -f x11grab -video_size $5 -r 30 -i :76 -f alsa -ac 2 -ar 44100 -i default /var/output/test.mp4 > /tmp/ffmpeg.log 2>&1 &
66 |
67 | wait_ready ffmpeg
68 |
69 | sleep $record_time
70 |
71 | # ffmpeg 必须先于 xvfb 退出
72 | echo "clean process ..."
73 | kill_pid ffmpeg 2
74 | wait_shutdown ffmpeg
75 |
76 | cat /tmp/ffmpeg.log
77 |
78 | ls -lh /var/output
79 |
80 | sleep 3s
81 |
82 | kill_pid record.js 15
83 | wait_shutdown record.js
84 | kill_pid Xvfb 15
85 | wait_shutdown Xvfb
86 | kill_pid chrome 15
87 | wait_shutdown chrome
88 | kill_pid xvfb-run 15
89 | wait_shutdown xvfb-run
90 | kill_pid pulseaudio 15
91 | wait_shutdown pulseaudio
92 |
93 | sleep 3s
94 |
95 | ps auxww
96 |
97 | echo "record worker finished!!!"
--------------------------------------------------------------------------------
/headless-ffmpeg/src/code/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Constants
4 | const PORT = 9000;
5 | const HOST = '0.0.0.0';
6 | const REQUEST_ID_HEADER = 'x-fc-request-id'
7 |
8 | var execSync = require("child_process").execSync;
9 | const OSS = require('ali-oss');
10 | const express = require('express');
11 | const app = express();
12 | app.use(express.raw());
13 |
14 | // invocation
15 | app.post('/invoke', (req, res) => {
16 | // console.log(JSON.stringify(req.headers));
17 | var rid = req.headers[REQUEST_ID_HEADER]
18 | console.log(`FC Invoke Start RequestId: ${rid}`)
19 | try {
20 | // get body to do your things
21 | var bodyStr = req.body.toString();
22 | console.log(bodyStr);
23 | var evt = JSON.parse(bodyStr);
24 | var recordTime = evt["record_time"];
25 | var videoUrl = evt["video_url"];
26 | var outputFile = evt["output_file"];
27 | var width = evt["width"];
28 | var height = evt["height"];
29 | var scale_factor = evt["scale"] || 1;
30 | var cmdStr = `/code/record.sh ${recordTime} '${videoUrl}' ${width}x${height}x24 ${width},${height} ${width}x${height} ${scale_factor}`;
31 | console.log(`cmd is ${cmdStr} \n`);
32 | execSync(cmdStr, {stdio: 'inherit', shell: "/bin/bash"});
33 | console.log("start upload video to oss ...");
34 | const store = new OSS({
35 | accessKeyId: process.env.OSS_AK_ID,
36 | accessKeySecret: process.env.OSS_AK_SECRET,
37 | bucket: process.env.OSS_BUCKET,
38 | endpoint: process.env.OSS_ENDPOINT,
39 | });
40 | store.put(outputFile, '/var/output/test.mp4').then((result) => {
41 | console.log("finish to upload video to oss");
42 | execSync("rm -rf /var/output/test.mp4", {stdio: 'inherit'});
43 | res.send('OK');
44 | console.log(`FC Invoke End RequestId: ${rid}`)
45 | }).catch(function (e) {
46 | res.status(404).send(e.stack || e);
47 | console.log(`FC Invoke End RequestId: ${rid}, Error: Unhandled function error`);
48 | });
49 | } catch (e) {
50 | res.status(404).send(e.stack || e);
51 | console.log(`FC Invoke End RequestId: ${rid}, Error: Unhandled function error`)
52 | }
53 | });
54 |
55 | var server = app.listen(PORT, HOST);
56 | console.log(`Running on http://${HOST}:${PORT}`);
57 |
58 | server.timeout = 0; // never timeout
59 | server.keepAliveTimeout = 0; // keepalive, never timeout
60 |
--------------------------------------------------------------------------------
/headless-ffmpeg/src/dest/fail/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation fail: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/headless-ffmpeg/src/dest/succ/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation success: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/headless-ffmpeg/src/readme.md:
--------------------------------------------------------------------------------
1 | # headless-ffmpeg 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署一个全景录制的应用到阿里云函数计算***
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## 部署 & 体验
32 |
33 |
34 |
35 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=headless-ffmpeg) ,
36 | [](https://fcnext.console.aliyun.com/applications/create?template=headless-ffmpeg) 该应用。
37 |
38 |
39 |
40 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
41 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
42 | - 初始化项目:`s init headless-ffmpeg -d headless-ffmpeg`
43 | - 进入项目,并进行项目部署:`cd headless-ffmpeg && s deploy -y`
44 |
45 |
46 |
47 |
48 |
49 | # 调用函数
50 |
51 | ``` bash
52 | # deploy
53 | $ s deploy -y --use-local
54 | # Invoke
55 | $ s invoke -e '{"record_time":"60","video_url":"https://tv.cctv.com/live/cctv1/","output_file":"record2/test.mp4", "width":"1920", "height":"1080", "scale": 0.75}'
56 | ```
57 |
58 | 调用成功后, 会在对应的 bucket 下, 产生 record/test.mp4 大约 60 秒 1920x1080 的全景录制视频。
59 |
60 | 其中参数的意义:
61 |
62 | **1.record_time:** 录制时长
63 |
64 | **2.video_url:** 录制视频的 url
65 |
66 | **3.width:** 录制视频的宽度
67 |
68 | **4.height:** 录制视频的高度
69 |
70 | **5.scale:** 浏览器缩放比例
71 |
72 | **6.output_file:** 最后录制视频保存的 OSS 目录
73 |
74 | 其中 scale 是对浏览器进行 75% 的缩放,使视频能录制更多的网页内容
75 |
76 | **注意:** 如果您录制的视频存在一些卡顿或者快进, 可能是因为您录制的视频分辨率大并且复杂, 消耗的 CPU 很大, 您可以通过调大函数的规格, 提高 CPU 的能力。
77 |
78 | 比如上面的示例参数得到下图:
79 |
80 | 
81 |
82 | # 如何本地调试
83 |
84 | 直接本地运行, 命令执行完毕后, 会在当前目录生成一个 test.mp4 的视频
85 |
86 | ```
87 | $ docker run --rm --entrypoint="" -v $(pwd):/var/output aliyunfc/browser_recorder /code/record.sh 60 https://tv.cctv.com/live/cctv1 1920x1080x24 1920,1080 1920x1080 1
88 | ```
89 |
90 | 调试
91 |
92 | ```bash
93 | # 如果有镜像有代码更新, 重新build 镜像
94 | $ docker build -t my-headless-ffmpeg -f ./code/Dockerfile ./code
95 | # 测试全屏录制核心脚本 record.sh, 执行完毕后, 会在当前目录有一个 test.mp4 的视频
96 | $ docker run --rm --entrypoint="" -v $(pwd):/var/output my-headless-ffmpeg /code/record.sh 60 https://tv.cctv.com/live/cctv1 1920x1080x24 1920,1080 1920x1080 1
97 | ```
98 |
99 | > 其中 record.sh 的参数意义:
100 | > 1. 录制时长
101 | > 2. 视频 url
102 | > 3. $widthx$heightx24
103 | > 4. $width,$height
104 | > 5. $widthx$height
105 | > 6. chrome 浏览器缩放比例
106 |
107 | # 原理
108 |
109 | Chrome 渲染到虚拟 X-server,并通过 FFmpeg 抓取系统桌⾯,通过启动 xvfb 启动虚拟 X-server,Chrome 进⾏全屏显示渲染到到虚拟 X-server 上,并通过 FFmpeg 抓取系统屏幕以及采集系统声⾳并进⾏编码写⽂件。这种⽅式的适配性⾮常好, 不仅可以录制 Chrome,理论上也可以录制其他的应⽤。缺点是占⽤的内存和 CPU 较多。
110 |
111 | **server.js**
112 |
113 | custom container http server 逻辑
114 |
115 | **record.sh**
116 |
117 | 核心录屏逻辑, 启动 xvfb, 在虚拟 X-server 中使用 `record.js` 中的 puppeteer 启动浏览器, 最后 FFmpeg 完成 X-server 屏幕的视频和音频抓取工作, 生成全屏录制后的视频
118 |
119 | # 其他
120 | 如果您想将生成的视频直接预热的 CDN, 以阿里云 CDN 为例, 只需要在 server.js 上传完 OSS bucket 后的逻辑中增加如下代码:
121 |
122 | [PushObjectCache](https://next.api.aliyun.com/api/Cdn/2018-05-10/PushObjectCache?lang=NODEJS&sdkStyle=old¶ms={})
123 |
124 | > Tips 前提需要配置好 CDN
125 |
126 |
127 |
128 |
129 |
130 |
131 | ## 开发者社区
132 |
133 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
134 |
135 |
136 |
137 | |
|
|
|
138 | |--- | --- | --- |
139 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/headless-ffmpeg/src/s.yaml:
--------------------------------------------------------------------------------
1 | # ------------------------------------
2 | # 欢迎您使用阿里云函数计算 FC 组件进行项目开发
3 | # 组件仓库地址:https://github.com/devsapp/fc
4 | # 组件帮助文档:https://www.serverless-devs.com/fc/readme
5 | # Yaml参考文档:https://www.serverless-devs.com/fc/yaml/readme
6 | # 关于:
7 | # - Serverless Devs和FC组件的关系、如何声明/部署多个函数、超过50M的代码包如何部署
8 | # - 关于.fcignore使用方法、工具中.s目录是做什么、函数进行build操作之后如何处理build的产物
9 | # 等问题,可以参考文档:https://www.serverless-devs.com/fc/tips
10 | # 关于如何做CICD等问题,可以参考:https://www.serverless-devs.com/serverless-devs/cicd
11 | # 关于如何进行环境划分等问题,可以参考:https://www.serverless-devs.com/serverless-devs/extend
12 | # 更多函数计算案例,可参考:https://github.com/devsapp/awesome/
13 | # 有问题快来钉钉群问一下吧:33947367
14 | # ------------------------------------
15 |
16 | edition: 1.0.0
17 | name: browser_video_recorder
18 | # access 是当前应用所需要的密钥信息配置:
19 | # 密钥配置可以参考:https://www.serverless-devs.com/serverless-devs/command/config
20 | # 密钥使用顺序可以参考:https://www.serverless-devs.com/serverless-devs/tool#密钥使用顺序与规范
21 | access: "{{ access }}"
22 |
23 | vars: # 全局变量
24 | region: "{{ region }}"
25 | service:
26 | name: "{{ serviceName }}"
27 | role: "{{ roleArn }}"
28 | description: 'Record a video for chrome browser'
29 | internetAccess: true
30 | functionName: "{{ functionName }}"
31 |
32 | services:
33 | browser_video_recorder_project: # 业务名称/模块名称
34 | component: fc # 组件名称,Serverless Devs 工具本身类似于一种游戏机,不具备具体的业务能力,组件类似于游戏卡,用户通过向游戏机中插入不同的游戏卡实现不同的功能,即通过使用不同的组件实现不同的具体业务能力
35 | actions:
36 | pre-deploy:
37 | - component: fc build --use-docker --dockerfile ./code/Dockerfile
38 | post-deploy:
39 | - component: fc api UpdateFunction --region ${vars.region} --header '{"x-fc-disable-container-reuse":"True"}' --path '{"serviceName":"${vars.service.name}","functionName":"${vars.functionName}"}'
40 | props:
41 | region: ${vars.region}
42 | service: ${vars.service}
43 | function:
44 | name: ${vars.functionName}
45 | runtime: custom-container
46 | memorySize: 8192
47 | instanceType: c1
48 | timeout: 7200
49 | customContainerConfig:
50 | image: "{{ acrRegistry }}"
51 | environmentVariables:
52 | OSS_AK_ID: "{{ ossAkID }}"
53 | OSS_AK_SECRET: "{{ ossAkSecret }}"
54 | OSS_BUCKET: "{{ ossBucket }}"
55 | OSS_ENDPOINT: oss-${vars.region}-internal.aliyuncs.com
56 | TZ: "{{ timeZone }}"
57 | asyncConfiguration:
58 | destination:
59 | # onSuccess: acs:fc:::services/${vars.service.name}/functions/dest-succ
60 | onFailure: acs:fc:::services/${vars.service.name}/functions/dest-fail
61 | maxAsyncEventAgeInSeconds: 18000
62 | maxAsyncRetryAttempts: 2
63 | statefulInvocation: true
64 |
65 | dest-succ: # 业务名称/模块名称
66 | component: fc
67 | props: # 组件的属性值
68 | region: ${vars.region}
69 | service: ${vars.service}
70 | function:
71 | name: dest-succ
72 | description: 'async task destination success function by serverless devs'
73 | runtime: python3
74 | codeUri: ./dest/succ
75 | handler: index.handler
76 | memorySize: 512
77 | timeout: 60
78 |
79 | dest-fail: # 业务名称/模块名称
80 | component: fc
81 | props: # 组件的属性值
82 | region: ${vars.region}
83 | service: ${vars.service}
84 | function:
85 | name: dest-fail
86 | description: 'async task destination fail function by serverless devs'
87 | runtime: python3
88 | codeUri: ./dest/fail
89 | handler: index.handler
90 | memorySize: 512
91 | timeout: 60
--------------------------------------------------------------------------------
/headless-ffmpeg/version.md:
--------------------------------------------------------------------------------
1 | - 第一版
2 |
--------------------------------------------------------------------------------
/http-transcode/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | .s
3 |
--------------------------------------------------------------------------------
/http-transcode/hook/index.js:
--------------------------------------------------------------------------------
1 | async function preInit(inputObj) {
2 |
3 | }
4 |
5 | async function postInit(inputObj) {
6 | console.log(`\n _______ _______ __ __ _______ _______ _______
7 | | || || |_| || || || |
8 | | ___|| ___|| || _ || ___|| ___|
9 | | |___ | |___ | || |_| || |___ | | __
10 | | ___|| ___|| || ___|| ___|| || |
11 | | | | | | ||_|| || | | |___ | |_| |
12 | |___| |___| |_| |_||___| |_______||_______|
13 | `)
14 | console.log(`\n Welcome to the ffmpeg-app application
15 | This application requires to open these services:
16 | FC : https://fc.console.aliyun.com/
17 | This application can help you quickly deploy the ffmpeg-app project.
18 | The application uses FC component:https://github.com/devsapp/fc
19 | The application homepage: https://github.com/devsapp/start-ffmpeg\n`)
20 | }
21 |
22 | module.exports = {
23 | postInit,
24 | preInit
25 | }
26 |
--------------------------------------------------------------------------------
/http-transcode/publish.yaml:
--------------------------------------------------------------------------------
1 | Type: Application
2 | Name: http-video-transcode
3 | Provider:
4 | - 阿里云
5 | Version: 0.0.11
6 | Description: 快速部署音视频转码的应用到阿里云函数计算
7 | HomePage: https://github.com/devsapp/start-ffmpeg
8 | Tags:
9 | - 部署函数
10 | - 音视频转码
11 | Category: 音视频处理
12 | Service:
13 | 函数计算:
14 | Authorities:
15 | - AliyunFCFullAccess
16 | Parameters:
17 | type: object
18 | additionalProperties: false # 不允许增加其他属性
19 | required: # 必填项
20 | - region
21 | - serviceName
22 | - roleArn
23 | - timeZone
24 | properties:
25 | region:
26 | title: 地域
27 | type: string
28 | default: cn-hangzhou
29 | description: 创建应用所在的地区
30 | enum:
31 | - cn-beijing
32 | - cn-hangzhou
33 | - cn-shanghai
34 | - cn-zhangjiakou
35 | - cn-shenzhen
36 | - cn-hongkong
37 | - ap-southeast-1
38 | - ap-southeast-2
39 | - eu-central-1
40 | - eu-west-1
41 | - us-west-1
42 | - us-east-1
43 | - ap-south-1
44 | serviceName:
45 | title: 服务名
46 | type: string
47 | default: VideoTranscoder-${default-suffix}
48 | pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$"
49 | description: 应用所属的函数计算服务
50 | required: true
51 | roleArn:
52 | title: RAM角色ARN
53 | type: string
54 | default: ''
55 | pattern: '^acs:ram::[0-9]*:role/.*$'
56 | description: "函数计算访问其他云服务时使用的服务角色,需要填写具体的角色ARN,格式为acs:ram::$account-id>:role/$role-name。例如:acs:ram::14310000000:role/aliyunfcdefaultrole。
57 | \n如果您没有特殊要求,可以使用函数计算提供的默认的服务角色,即AliyunFCDefaultRole, 并增加 AliyunOSSFullAccess 权限。如果您首次使用函数计算,可以访问 https://fcnext.console.aliyun.com 进行授权。
58 | \n详细文档参考 https://help.aliyun.com/document_detail/181589.html#section-o93-dbr-z6o"
59 | required: true
60 | x-role:
61 | name: fctranscoderole
62 | service: fc
63 | authorities:
64 | - AliyunOSSFullAccess
65 | - AliyunFCDefaultRolePolicy
66 | timeZone:
67 | title: 时区
68 | type: string
69 | default: Asia/Shanghai
70 | description: 创建的应用函数执行时候所在实例的时区, 详情参考 https://docs.oracle.com/middleware/12211/wcs/tag-ref/MISC/TimeZones.html
71 | required: true
72 |
--------------------------------------------------------------------------------
/http-transcode/readme.md:
--------------------------------------------------------------------------------
1 | # http-video-transcode 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署音视频转码的应用到阿里云函数计算***
18 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ## 部署 & 体验
34 |
35 |
36 |
37 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=http-video-transcode) ,
38 | [](https://fcnext.console.aliyun.com/applications/create?template=http-video-transcode) 该应用。
39 |
40 |
41 |
42 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
43 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
44 | - 初始化项目:`s init http-video-transcode -d http-video-transcode`
45 | - 进入项目,并进行项目部署:`cd http-video-transcode && s deploy -y`
46 |
47 |
48 |
49 |
50 |
51 | # 应用详情
52 |
53 | ### 调用函数
54 |
55 | 1. 发起 5 次异步任务函数调用
56 |
57 | ```bash
58 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"mov"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
59 |
60 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"mov"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
61 |
62 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"flv"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
63 |
64 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"avi"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
65 |
66 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"m3u8"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
67 |
68 | ```
69 |
70 | 2. 登录[FC 控制台](https://fcnext.console.aliyun.com/)
71 |
72 | 
73 |
74 | 可以清晰看出每一次转码任务的执行情况:
75 |
76 | - A 视频是什么时候开始转码的, 什么时候转码结束
77 | - B 视频转码任务不太符合预期, 我中途可以点击停止调用
78 | - 通过调用状态过滤和时间窗口过滤,我可以知道现在有多少个任务正在执行, 历史完成情况是怎么样的
79 | - 可以追溯每次转码任务执行日志和触发payload
80 | - 当您的转码函数有异常时候, 会触发 dest-fail 函数的执行,您在这个函数可以添加您自定义的逻辑, 比如报警
81 | - ...
82 |
83 | 转码完毕后, 您也可以登录 OSS 控制台到指定的输出目录查看转码后的视频。
84 |
85 | > 在本地使用该项目时,不仅可以部署,还可以进行更多的操作,例如查看日志,查看指标,进行多种模式的调试等,这些操作详情可以参考[函数计算组件命令文档](https://github.com/devsapp/fc#%E6%96%87%E6%A1%A3%E7%9B%B8%E5%85%B3) ;
86 |
87 | ## 应用详情
88 |
89 | 本项目是基于函数计算打造一个 **Serverless架构的弹性高可用音视频处理系统**, 并且拥有以下优势:
90 |
91 | ### 拥有函数计算和Serverless工作流两个产品的优势
92 |
93 | * 无需采购和管理服务器等基础设施,只需专注视频处理业务逻辑的开发,大幅缩短项目交付时间、减少人力成本。
94 |
95 | * 提供日志查询、性能监控、报警等功能,可以快速排查故障。
96 |
97 | * 以事件驱动的方式触发响应请求。
98 |
99 | * 免运维,毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力,性能优异。
100 |
101 | * 成本极具竞争力。
102 |
103 |
104 |
105 | ### 相较于通用的转码处理服务的优点
106 |
107 | * 超强自定义,对用户透明,基于FFmpeg或其他音视频处理工具命令快速开发相应的音视频处理逻辑。
108 |
109 | * 一键迁移原基于FFmpeg自建的音视频处理服务。
110 |
111 | * 弹性更强,可以保证有充足的计算资源为转码服务,例如每周五定期产生几百个4 GB以上的1080P大视频,但是需要几小时内全部处理。
112 |
113 | * 音频格式的转换或各种采样率自定义、音频降噪等功能。例如专业音频处理工具AACgain和MP3Gain。
114 |
115 | * 可以和Serverless工作流完成更加复杂、自定义的任务编排。例如视频转码完成后,记录转码详情到数据库,同时自动将热度很高的视频预热到CDN上,从而缓解源站压力。
116 |
117 | * 更多方式的事件驱动,例如可以选择OSS自动触发,也可以根据业务选择MNS消息触发。
118 |
119 | * 在大部分场景下具有很强的成本竞争力。
120 |
121 |
122 |
123 | ### 相比于其他自建服务的优点
124 |
125 | * 毫秒级弹性伸缩,弹性能力超强,支持大规模资源调用,可弹性支持几万核的计算力,例如1万节课半个小时内完成转码。
126 |
127 | * 只需要专注业务逻辑代码即可,原生自带事件驱动模式,简化开发编程模型,同时可以达到消息,即音视频任务,处理的优先级,可大大提高开发运维效率。
128 |
129 | * 函数计算采用3AZ部署,安全性高,计算资源也是多AZ获取,能保证每位使用者需要的算力峰值。
130 |
131 | * 开箱即用的监控系统,可以多维度监控函数的执行情况,根据监控快速定位问题,同时给您提供分析能力。
132 |
133 | * 在大部分场景下具有很强的成本竞争力,因为函数计算是真正的按量付费,计费粒度在百毫秒,可以理解为CPU的利用率为100%。
134 |
135 |
136 | 通过 Serverless Devs 开发者工具,您只需要几步,就可以体验 Serverless 架构,带来的降本提效的技术红利。
137 |
138 |
139 |
140 |
141 |
142 |
143 | ## 开发者社区
144 |
145 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
146 |
147 |
148 |
149 | |
|
|
|
150 | |--- | --- | --- |
151 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/http-transcode/src/code/fail/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation fail: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/http-transcode/src/code/succ/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation success: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/http-transcode/src/code/transcode/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 | import oss2
4 | import os
5 | import json
6 | import subprocess
7 | import shutil
8 |
9 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
10 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
11 | LOGGER = logging.getLogger()
12 |
13 |
14 | def get_fileNameExt(filename):
15 | (_, tempfilename) = os.path.split(filename)
16 | (shortname, extension) = os.path.splitext(tempfilename)
17 | return shortname, extension
18 |
19 |
20 | def handler(environ, start_response):
21 | context = environ['fc.context']
22 | # get request_body
23 | try:
24 | request_body_size = int(environ.get('CONTENT_LENGTH', 0))
25 | except (ValueError):
26 | request_body_size = 0
27 | event = environ['wsgi.input'].read(request_body_size)
28 |
29 | evt = json.loads(event)
30 | oss_bucket_name = evt["bucket"]
31 | object_key = evt["object"]
32 | output_dir = evt["output_dir"]
33 | dst_format = evt['dst_format']
34 | shortname, _ = get_fileNameExt(object_key)
35 | creds = context.credentials
36 | auth = oss2.StsAuth(creds.accessKeyId,
37 | creds.accessKeySecret, creds.securityToken)
38 | oss_client = oss2.Bucket(auth, 'oss-%s-internal.aliyuncs.com' %
39 | context.region, oss_bucket_name)
40 |
41 | # simplifiedmeta = oss_client.get_object_meta(object_key)
42 | # size = float(simplifiedmeta.headers['Content-Length'])
43 | # M_size = round(size / 1024.0 / 1024.0, 2)
44 |
45 | exist = oss_client.object_exists(object_key)
46 | if not exist:
47 | raise Exception("object {} is not exist".format(object_key))
48 |
49 | input_path = oss_client.sign_url('GET', object_key, 6 * 3600)
50 | # m3u8 特殊处理
51 | rid = context.request_id
52 | if dst_format == "m3u8":
53 | return handle_m3u8(rid, oss_client, input_path, shortname, output_dir)
54 | else:
55 | return handle_common(rid, oss_client, input_path, shortname, output_dir, dst_format)
56 |
57 |
58 | def handle_m3u8(request_id, oss_client, input_path, shortname, output_dir):
59 | ts_dir = '/tmp/ts'
60 | if os.path.exists(ts_dir):
61 | shutil.rmtree(ts_dir)
62 | os.mkdir(ts_dir)
63 | transcoded_filepath = os.path.join('/tmp', shortname + '.ts')
64 | split_transcoded_filepath = os.path.join(
65 | ts_dir, shortname + '_%03d.ts')
66 | cmd1 = ['ffmpeg', '-y', '-i', input_path, '-c:v',
67 | 'libx264', transcoded_filepath]
68 |
69 | cmd2 = ['ffmpeg', '-y', '-i', transcoded_filepath, '-c', 'copy', '-map', '0', '-f', 'segment',
70 | '-segment_list', os.path.join(ts_dir, 'playlist.m3u8'), '-segment_time', '10', split_transcoded_filepath]
71 |
72 | try:
73 | subprocess.run(
74 | cmd1, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
75 |
76 | subprocess.run(
77 | cmd2, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
78 |
79 | for filename in os.listdir(ts_dir):
80 | filepath = os.path.join(ts_dir, filename)
81 | filekey = os.path.join(output_dir, shortname, filename)
82 | oss_client.put_object_from_file(filekey, filepath)
83 | os.remove(filepath)
84 | print("Uploaded {} to {}".format(filepath, filekey))
85 |
86 | except subprocess.CalledProcessError as exc:
87 | # if transcode fail,trigger invoke dest-fail function
88 | raise Exception(request_id +
89 | " transcode failure, detail: " + str(exc))
90 |
91 | finally:
92 | if os.path.exists(ts_dir):
93 | shutil.rmtree(ts_dir)
94 |
95 | # remove ts 文件
96 | if os.path.exists(transcoded_filepath):
97 | os.remove(transcoded_filepath)
98 | return {}
99 |
100 |
101 | def handle_common(request_id, oss_client, input_path, shortname, output_dir, dst_format):
102 | transcoded_filepath = os.path.join('/tmp', shortname + '.' + dst_format)
103 | if os.path.exists(transcoded_filepath):
104 | os.remove(transcoded_filepath)
105 | cmd = ["ffmpeg", "-y", "-i", input_path, transcoded_filepath]
106 | try:
107 | subprocess.run(
108 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
109 |
110 | oss_client.put_object_from_file(
111 | os.path.join(output_dir, shortname + '.' + dst_format), transcoded_filepath)
112 | except subprocess.CalledProcessError as exc:
113 | # if transcode fail,trigger invoke dest-fail function
114 | raise Exception(request_id +
115 | " transcode failure, detail: " + str(exc))
116 | finally:
117 | if os.path.exists(transcoded_filepath):
118 | os.remove(transcoded_filepath)
119 | return {}
120 |
--------------------------------------------------------------------------------
/http-transcode/src/readme.md:
--------------------------------------------------------------------------------
1 | # http-video-transcode 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署音视频转码的应用到阿里云函数计算***
18 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ## 部署 & 体验
34 |
35 |
36 |
37 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=http-video-transcode) ,
38 | [](https://fcnext.console.aliyun.com/applications/create?template=http-video-transcode) 该应用。
39 |
40 |
41 |
42 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
43 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
44 | - 初始化项目:`s init http-video-transcode -d http-video-transcode`
45 | - 进入项目,并进行项目部署:`cd http-video-transcode && s deploy -y`
46 |
47 |
48 |
49 |
50 |
51 | # 应用详情
52 |
53 | ### 调用函数
54 |
55 | 1. 发起 5 次异步任务函数调用
56 |
57 | ```bash
58 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"mov"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
59 |
60 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"mov"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
61 |
62 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"flv"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
63 |
64 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"avi"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
65 |
66 | $ curl -v -H "X-Fc-Invocation-Type: Async" -H "Content-Type: application/json" -d '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"m3u8"}' -X POST https://http-***.cn-shenzhen.fcapp.run/
67 |
68 | ```
69 |
70 | 2. 登录[FC 控制台](https://fcnext.console.aliyun.com/)
71 |
72 | 
73 |
74 | 可以清晰看出每一次转码任务的执行情况:
75 |
76 | - A 视频是什么时候开始转码的, 什么时候转码结束
77 | - B 视频转码任务不太符合预期, 我中途可以点击停止调用
78 | - 通过调用状态过滤和时间窗口过滤,我可以知道现在有多少个任务正在执行, 历史完成情况是怎么样的
79 | - 可以追溯每次转码任务执行日志和触发payload
80 | - 当您的转码函数有异常时候, 会触发 dest-fail 函数的执行,您在这个函数可以添加您自定义的逻辑, 比如报警
81 | - ...
82 |
83 | 转码完毕后, 您也可以登录 OSS 控制台到指定的输出目录查看转码后的视频。
84 |
85 | > 在本地使用该项目时,不仅可以部署,还可以进行更多的操作,例如查看日志,查看指标,进行多种模式的调试等,这些操作详情可以参考[函数计算组件命令文档](https://github.com/devsapp/fc#%E6%96%87%E6%A1%A3%E7%9B%B8%E5%85%B3) ;
86 |
87 | ## 应用详情
88 |
89 | 本项目是基于函数计算打造一个 **Serverless架构的弹性高可用音视频处理系统**, 并且拥有以下优势:
90 |
91 | ### 拥有函数计算和Serverless工作流两个产品的优势
92 |
93 | * 无需采购和管理服务器等基础设施,只需专注视频处理业务逻辑的开发,大幅缩短项目交付时间、减少人力成本。
94 |
95 | * 提供日志查询、性能监控、报警等功能,可以快速排查故障。
96 |
97 | * 以事件驱动的方式触发响应请求。
98 |
99 | * 免运维,毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力,性能优异。
100 |
101 | * 成本极具竞争力。
102 |
103 |
104 |
105 | ### 相较于通用的转码处理服务的优点
106 |
107 | * 超强自定义,对用户透明,基于FFmpeg或其他音视频处理工具命令快速开发相应的音视频处理逻辑。
108 |
109 | * 一键迁移原基于FFmpeg自建的音视频处理服务。
110 |
111 | * 弹性更强,可以保证有充足的计算资源为转码服务,例如每周五定期产生几百个4 GB以上的1080P大视频,但是需要几小时内全部处理。
112 |
113 | * 音频格式的转换或各种采样率自定义、音频降噪等功能。例如专业音频处理工具AACgain和MP3Gain。
114 |
115 | * 可以和Serverless工作流完成更加复杂、自定义的任务编排。例如视频转码完成后,记录转码详情到数据库,同时自动将热度很高的视频预热到CDN上,从而缓解源站压力。
116 |
117 | * 更多方式的事件驱动,例如可以选择OSS自动触发,也可以根据业务选择MNS消息触发。
118 |
119 | * 在大部分场景下具有很强的成本竞争力。
120 |
121 |
122 |
123 | ### 相比于其他自建服务的优点
124 |
125 | * 毫秒级弹性伸缩,弹性能力超强,支持大规模资源调用,可弹性支持几万核的计算力,例如1万节课半个小时内完成转码。
126 |
127 | * 只需要专注业务逻辑代码即可,原生自带事件驱动模式,简化开发编程模型,同时可以达到消息,即音视频任务,处理的优先级,可大大提高开发运维效率。
128 |
129 | * 函数计算采用3AZ部署,安全性高,计算资源也是多AZ获取,能保证每位使用者需要的算力峰值。
130 |
131 | * 开箱即用的监控系统,可以多维度监控函数的执行情况,根据监控快速定位问题,同时给您提供分析能力。
132 |
133 | * 在大部分场景下具有很强的成本竞争力,因为函数计算是真正的按量付费,计费粒度在百毫秒,可以理解为CPU的利用率为100%。
134 |
135 |
136 | 通过 Serverless Devs 开发者工具,您只需要几步,就可以体验 Serverless 架构,带来的降本提效的技术红利。
137 |
138 |
139 |
140 |
141 |
142 |
143 | ## 开发者社区
144 |
145 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
146 |
147 |
148 |
149 | |
|
|
|
150 | |--- | --- | --- |
151 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/http-transcode/src/s.yaml:
--------------------------------------------------------------------------------
1 | # ------------------------------------
2 | # 欢迎您使用阿里云函数计算 FC 组件进行项目开发
3 | # 组件仓库地址:https://github.com/devsapp/fc
4 | # 组件帮助文档:https://www.serverless-devs.com/fc/readme
5 | # Yaml参考文档:https://www.serverless-devs.com/fc/yaml/readme
6 | # 关于:
7 | # - Serverless Devs和FC组件的关系、如何声明/部署多个函数、超过50M的代码包如何部署
8 | # - 关于.fcignore使用方法、工具中.s目录是做什么、函数进行build操作之后如何处理build的产物
9 | # 等问题,可以参考文档:https://www.serverless-devs.com/fc/tips
10 | # 关于如何做CICD等问题,可以参考:https://www.serverless-devs.com/serverless-devs/cicd
11 | # 关于如何进行环境划分等问题,可以参考:https://www.serverless-devs.com/serverless-devs/extend
12 | # 更多函数计算案例,可参考:https://github.com/devsapp/awesome/
13 | # 有问题快来钉钉群问一下吧:33947367
14 | # ------------------------------------
15 |
16 | edition: 1.0.0
17 | name: http-video-transcode
18 | # access 是当前应用所需要的密钥信息配置:
19 | # 密钥配置可以参考:https://www.serverless-devs.com/serverless-devs/command/config
20 | # 密钥使用顺序可以参考:https://www.serverless-devs.com/serverless-devs/tool#密钥使用顺序与规范
21 | access: "{{ access }}"
22 |
23 |
24 | vars:
25 | region: "{{ region }}"
26 | service:
27 | name: "{{ serviceName }}"
28 | description: use ffmpeg to transcode video in FC
29 | internetAccess: true
30 | role: "{{ roleArn }}"
31 | # logConfig: auto
32 |
33 | services:
34 | VideoTranscoder: # 业务名称/模块名称
35 | component: fc # 组件名称,Serverless Devs 工具本身类似于一种游戏机,不具备具体的业务能力,组件类似于游戏卡,用户通过向游戏机中插入不同的游戏卡实现不同的功能,即通过使用不同的组件实现不同的具体业务能力
36 | # actions: # 自定义执行逻辑,关于actions 的使用,可以参考:https://www.serverless-devs.com/serverless-devs/yaml#行为描述
37 | # pre-deploy: # 在deploy之前运行
38 | # - run: s version publish -a demo
39 | # path: ./src
40 | # - run: docker build xxx # 要执行的系统命令,类似于一种钩子的形式
41 | # path: ./src # 执行系统命令/钩子的路径
42 | # - plugin: myplugin # 与运行的插件 (可以通过s cli registry search --type Plugin 获取组件列表)
43 | # args: # 插件的参数信息
44 | # testKey: testValue
45 | props:
46 | region: ${vars.region}
47 | service: ${vars.service}
48 | function:
49 | name: transcode
50 | runtime: python3
51 | Handler: index.handler
52 | codeUri: ./code/transcode
53 | memorySize: 8192
54 | timeout: 7200
55 | instanceType: c1
56 | environmentVariables:
57 | TZ: "{{ timeZone }}"
58 | asyncConfiguration:
59 | destination:
60 | # onSuccess: acs:fc:::services/${vars.service.name}/functions/dest-succ
61 | onFailure: acs:fc:::services/${vars.service.name}/functions/dest-fail
62 | maxAsyncEventAgeInSeconds: 18000
63 | maxAsyncRetryAttempts: 2
64 | statefulInvocation: true
65 | triggers:
66 | - name: httpTrigger
67 | type: http
68 | config:
69 | authType: anonymous
70 | methods:
71 | - GET
72 | - POST
73 | - PUT
74 | - DELETE
75 |
76 | dest-succ: # 业务名称/模块名称
77 | component: fc
78 | props: # 组件的属性值
79 | region: ${vars.region}
80 | service: ${vars.service}
81 | function:
82 | name: dest-succ
83 | description: 'async task destination success function by serverless devs'
84 | runtime: python3
85 | codeUri: ./code/succ
86 | handler: index.handler
87 | memorySize: 512
88 | timeout: 60
89 |
90 | dest-fail: # 业务名称/模块名称
91 | component: fc
92 | props: # 组件的属性值
93 | region: ${vars.region}
94 | service: ${vars.service}
95 | function:
96 | name: dest-fail
97 | description: 'async task destination fail function by serverless devs'
98 | runtime: python3
99 | codeUri: ./code/fail
100 | handler: index.handler
101 | memorySize: 512
102 | timeout: 60
103 |
104 | # next-function: # 第二个函数的案例,仅供参考
105 | # # 如果在当前项目下执行 s deploy,会同时部署模块:
106 | # # helloworld:服务hello-world-service,函数cpp-event-function
107 | # # next-function:服务hello-world-service,函数next-function-example
108 | # # 如果想单独部署当前服务与函数,可以执行 s + 模块名/业务名 + deploy,例如:s next-function deploy
109 | # # 如果想单独部署当前函数,可以执行 s + 模块名/业务名 + deploy function,例如:s next-function deploy function
110 | # # 更多命令可参考:https://www.serverless-devs.com/fc/readme#文档相关
111 | # component: fc
112 | # props:
113 | # region: ${vars.region}
114 | # service: ${vars.service} # 应用整体的服务配置
115 | # function: # 定义一个新的函数
116 | # name: next-function-example
117 | # description: 'hello world by serverless devs'
--------------------------------------------------------------------------------
/http-transcode/version.md:
--------------------------------------------------------------------------------
1 | - 第一版
2 |
--------------------------------------------------------------------------------
/publish.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | import time
3 |
4 |
5 | def getContent(fileList):
6 | for eveFile in fileList:
7 | try:
8 | with open(eveFile) as f:
9 | return f.read()
10 | except:
11 | pass
12 | return None
13 |
14 |
15 | with open('update.list') as f:
16 | publish_list = [eve_app.strip() for eve_app in f.readlines()]
17 |
18 | for eve_app in publish_list:
19 | times = 1
20 | while times <= 3:
21 | print("----------------------: ", eve_app)
22 | publish_script = 'https://serverless-registry.oss-cn-hangzhou.aliyuncs.com/publish-file/python3/hub-publish.py'
23 | command = 'cd %s && wget %s && python hub-publish.py' % (
24 | eve_app, publish_script)
25 | child = subprocess.Popen(
26 | command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, )
27 | stdout, stderr = child.communicate()
28 | if child.returncode == 0:
29 | print("stdout:", stdout.decode("utf-8"))
30 | break
31 | else:
32 | print("stdout:", stdout.decode("utf-8"))
33 | print("stderr:", stderr.decode("utf-8"))
34 | time.sleep(3)
35 | if times == 3:
36 | raise ChildProcessError(stderr)
37 | times = times + 1
38 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # FFmpeg案例
2 |
3 | 基于函数计算 FC + FFmpeg 实现 Serverless 架构的弹性高可用的高度自定义音视频处理主题。
4 |
5 | 本主题一共包括三个部分:
6 |
7 | - [FFmpeg的基本操作案例](./ffmpeg-app/src): `s init ffmpeg-app`
8 | - AudioConvert: 音频格式转换器 ([案例代码](./ffmpeg-app/src/functions/audio_convert))
9 | - GetMediaMeta: 获取音视频 meta ([案例代码](./ffmpeg-app/src/functions/get_multimedia_meta))
10 | - GetDuration: 获取音视频时长 ([案例代码](./ffmpeg-app/src/functions/get_duration))
11 | - VideoGif: 功能强大的 video 提取为 gif 函数 ([案例代码](./ffmpeg-app/src/functions/video_gif))
12 | - GetSprites: 功能强大雪碧图制作函数 ([案例代码](./ffmpeg-app/src/functions/get_sprites))
13 | - VideoWatermark: 功能强大的视频添加水印功能 ([案例代码](./ffmpeg-app/src/functions/video_watermark))
14 | - [基于 FFmpeg 实现音视频转码](./transcode/src): `s init video-transcode`
15 | - [基于 FFmpeg 实现 HTTP 触发器触发音视频转码](./http-transcode/src): `s init http-video-transcode`
16 | - [基于 FC + Serverless Workflow + OSS + NAS + FFmpeg 实现的弹性高可用、并行处理的视频转码服务](./video-process-flow/src): `s init video-process-flow`
17 | - [对直播视频流截图的应用](./rtmp-snapshot/src): `s init rtmp-snapshot`
18 | - [一个对浏览器全景录制](./headless-ffmpeg/src): `s init headless-ffmpeg`
19 |
--------------------------------------------------------------------------------
/rtmp-snapshot/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | .s
3 |
--------------------------------------------------------------------------------
/rtmp-snapshot/hook/index.js:
--------------------------------------------------------------------------------
1 | async function preInit(inputObj) {
2 | console.log(`\n _______ _______ __ __ _______ _______ _______
3 | | || || |_| || || || |
4 | | ___|| ___|| || _ || ___|| ___|
5 | | |___ | |___ | || |_| || |___ | | __
6 | | ___|| ___|| || ___|| ___|| || |
7 | | | | | | ||_|| || | | |___ | |_| |
8 | |___| |___| |_| |_||___| |_______||_______|
9 | `)
10 | }
11 |
12 | async function postInit(inputObj) {
13 | console.log(`\n Welcome to the ffmpeg-app application
14 | This application requires to open these services:
15 | FC : https://fc.console.aliyun.com/
16 | OSS: https://oss.console.aliyun.com/
17 |
18 | * 关于项目的介绍,可以参考:https://github.com/devsapp/start-ffmpeg/blob/master/rtmp-snapshot/src
19 | * 项目初始化完成,您可以直接进入项目目录下
20 | 1. 对s.yaml进行升级,例如填充好environmentVariables中的部分变量值(OSS存储桶相关信息)
21 | 2. 进行构建:s build --use-docker --dockerfile ./code/Dockerfile
22 | 3. 项目部署:s deploy --use-local -y
23 | * 最后您还可以验证项目的正确性:
24 | https://github.com/devsapp/start-ffmpeg/tree/master/rtmp-snapshot/src#%E6%B5%8B%E8%AF%95\n`)
25 | }
26 |
27 | module.exports = {
28 | postInit,
29 | preInit
30 | }
31 |
--------------------------------------------------------------------------------
/rtmp-snapshot/publish.yaml:
--------------------------------------------------------------------------------
1 | Type: Application
2 | Name: rtmp-snapshot
3 | Provider:
4 | - 阿里云
5 | Version: 0.1.10
6 | Description: 基于FFmpeg对直播视频流进行截图
7 | HomePage: https://github.com/devsapp/start-ffmpeg/tree/master/rtmp-snapshot
8 | Tags:
9 | - ffmpeg
10 | - 直播视频视频流
11 | - 截图
12 | Category: 音视频处理
13 | Service:
14 | 函数计算:
15 | Authorities:
16 | - AliyunFCFullAccess
17 | Parameters:
18 | type: object
19 | additionalProperties: false # 不允许增加其他属性
20 | required: # 必填项
21 | - region
22 | - serviceName
23 | - roleArn
24 | properties:
25 | region:
26 | title: 地域
27 | type: string
28 | default: cn-hangzhou
29 | description: 创建应用所在的地区
30 | enum:
31 | - cn-beijing
32 | - cn-hangzhou
33 | - cn-shanghai
34 | - cn-qingdao
35 | - cn-zhangjiakou
36 | - cn-huhehaote
37 | - cn-shenzhen
38 | - cn-chengdu
39 | - cn-hongkong
40 | - ap-southeast-1
41 | - ap-southeast-2
42 | - ap-southeast-3
43 | - ap-southeast-5
44 | - ap-northeast-1
45 | - eu-central-1
46 | - eu-west-1
47 | - us-west-1
48 | - us-east-1
49 | - ap-south-1
50 | serviceName:
51 | title: 服务名
52 | type: string
53 | default: rtmp-snapshot-${default-suffix}
54 | pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$"
55 | description: 应用所属的函数计算服务
56 | required: true
57 | roleArn:
58 | title: RAM角色ARN
59 | type: string
60 | default: ''
61 | pattern: '^acs:ram::[0-9]*:role/.*$'
62 | description: "函数计算访问其他云服务时使用的服务角色,需要填写具体的角色ARN,格式为acs:ram::$account-id>:role/$role-name。例如:acs:ram::14310000000:role/aliyunfcdefaultrole。
63 | \n如果您没有特殊要求,可以使用函数计算提供的默认的服务角色,即AliyunFCDefaultRole, 并增加 AliyunOSSFullAccess 权限。如果您首次使用函数计算,可以访问 https://fcnext.console.aliyun.com 进行授权。
64 | \n详细文档参考 https://help.aliyun.com/document_detail/181589.html#section-o93-dbr-z6o"
65 | required: true
66 | x-role:
67 | name: fcffmpegrole
68 | service: fc
69 | authorities:
70 | - AliyunOSSFullAccess
71 | - AliyunFCDefaultRolePolicy
72 | timeZone:
73 | title: 时区
74 | type: string
75 | default: Asia/Shanghai
76 | description: 创建的应用函数执行时候所在实例的时区, 详情参考 https://docs.oracle.com/middleware/12211/wcs/tag-ref/MISC/TimeZones.html
77 | required: true
78 |
--------------------------------------------------------------------------------
/rtmp-snapshot/readme.md:
--------------------------------------------------------------------------------
1 | # rtmp-snapshot 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署直播视频流截图的应用到阿里云函数计算***
18 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ## 部署 & 体验
34 |
35 |
36 |
37 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=rtmp-snapshot) ,
38 | [](https://fcnext.console.aliyun.com/applications/create?template=rtmp-snapshot) 该应用。
39 |
40 |
41 |
42 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
43 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
44 | - 初始化项目:`s init rtmp-snapshot -d rtmp-snapshot`
45 | - 进入项目,并进行项目部署:`cd rtmp-snapshot && s deploy -y`
46 |
47 |
48 |
49 |
50 |
51 | # 应用详情
52 |
53 | ## 测试
54 |
55 | 比如在 ecs 部署了一个简单的直播服务器, IP 是 101.200.48.101
56 |
57 | ```bash
58 | docker run -it -p 1935:1935 -p 8080:80 --rm alfg/nginx-rtmp
59 | ```
60 |
61 | #### 发起推流
62 |
63 | 比如测试视频 test.flv 大约为 10 分钟左右的视频
64 |
65 | ```
66 | ffmpeg -re -i test.flv -vcodec copy -acodec aac -ar 44100 -f flv rtmp://101.200.48.101:1935/stream/example
67 | ```
68 |
69 | 播放器输入这个地址就可以查看了:
70 |
71 | `rtmp://101.200.48.101:1935/stream/example`
72 |
73 | 比如, 截一张图:
74 |
75 | ```
76 | ffmpeg -i rtmp://101.200.48.101:1935/stream/example -frames:v 1 1.png
77 | ```
78 |
79 | 持续截图:
80 |
81 | ```
82 | ffmpeg -i rtmp://101.200.48.101:1935/stream/example -f image2 -r 1 -strftime 1 /tmp/%Y%m%d%H%M%S.jpg
83 | ```
84 |
85 | #### 调用函数
86 |
87 | ##### 1. 异步调用函数
88 |
89 | ```bash
90 | $ s invoke -e '{"rtmp_url" : "rtmp://101.200.48.101:1935/stream/example", "bucket":"my-bucket", "region":"cn-hangzhou", "dst":"dst"}' --invocation-type async
91 | ```
92 |
93 | 其中:
94 |
95 | - **rtmp_url:** 必需, 直播流地址
96 |
97 | - **bucket:** 必需, 保存截图文件的 bucket 名字
98 |
99 | - **region:** 可选,bucket 的 region, 不填默认使用函数所在的 region
100 |
101 | - **dst:** 可选,保存截图文件 bucket 的指定目录, 不填默认为空, 即根目录
102 |
103 | 发起推流后, 函数执行是根据推流是否结束, 推流结束了, 然后函数里面的 ffmpeg 截图命令也就结束了, 最后将 /tmp 下面的图片保存回 oss
104 |
105 |
106 | ##### 2. 登录[FC 控制台](https://fcnext.console.aliyun.com/),查看截图任务函数执行详情
107 |
108 | 
109 |
110 | ## 进阶
111 |
112 | 如果有边截图, 边上传回 oss 的需求, 可以基于这个代码再优化下
113 |
114 |
115 |
116 |
117 |
118 |
119 | ## 开发者社区
120 |
121 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
122 |
123 |
124 |
125 | |
|
|
|
126 | |--- | --- | --- |
127 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/rtmp-snapshot/src/code/fail/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation fail: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/rtmp-snapshot/src/code/snapshot/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 | import subprocess
4 | import os
5 | import oss2
6 | import json
7 |
8 |
9 | def handler(event, context):
10 | logger = logging.getLogger()
11 | # clear /tmp
12 | os.system("cd /tmp && rm -rf *")
13 | evt = json.loads(event)
14 | # for example, input_path = "rtmp://101.200.48.101:1935/stream/example"
15 | input_path = evt["rtmp_url"]
16 | transcoded_filepath = "/tmp/%Y%m%d%H%M%S.jpg"
17 | cmd = [
18 | "ffmpeg", "-y", "-i", input_path, "-f", "image2", "-r", "1",
19 | "-strftime", "1", transcoded_filepath
20 | ]
21 | try:
22 | subprocess.run(cmd,
23 | stdout=subprocess.PIPE,
24 | stderr=subprocess.PIPE,
25 | check=True)
26 | except subprocess.CalledProcessError as exc:
27 | raise Exception(context.request_id +
28 | " rtmp snapshot failure, detail: " + str(exc))
29 |
30 | bucketName = evt['bucket']
31 | region = evt.get('region', context.region)
32 | endpoint = 'http://oss-{}.aliyuncs.com'.format(region)
33 | if region == context.region:
34 | endpoint = 'http://oss-{}-internal.aliyuncs.com'.format(region)
35 | creds = context.credentials
36 | auth = oss2.StsAuth(creds.access_key_id,
37 | creds.access_key_secret, creds.security_token)
38 | bucket = oss2.Bucket(auth, endpoint, bucketName)
39 |
40 | logger.info('upload pictures to OSS ...')
41 | dst = evt.get('dst', '')
42 | for filename in os.listdir("/tmp"): # /tmp 目录下面都是生成的 jpg 图片
43 | bucket.put_object_from_file(os.path.join(
44 | dst, filename), "/tmp/{}".format(filename))
45 |
46 | return 'SUCC'
47 |
--------------------------------------------------------------------------------
/rtmp-snapshot/src/code/succ/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation success: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/rtmp-snapshot/src/readme.md:
--------------------------------------------------------------------------------
1 | # rtmp-snapshot 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署直播视频流截图的应用到阿里云函数计算***
18 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ## 部署 & 体验
34 |
35 |
36 |
37 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=rtmp-snapshot) ,
38 | [](https://fcnext.console.aliyun.com/applications/create?template=rtmp-snapshot) 该应用。
39 |
40 |
41 |
42 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
43 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
44 | - 初始化项目:`s init rtmp-snapshot -d rtmp-snapshot`
45 | - 进入项目,并进行项目部署:`cd rtmp-snapshot && s deploy -y`
46 |
47 |
48 |
49 |
50 |
51 | # 应用详情
52 |
53 | ## 测试
54 |
55 | 比如在 ecs 部署了一个简单的直播服务器, IP 是 101.200.48.101
56 |
57 | ```bash
58 | docker run -it -p 1935:1935 -p 8080:80 --rm alfg/nginx-rtmp
59 | ```
60 |
61 | #### 发起推流
62 |
63 | 比如测试视频 test.flv 大约为 10 分钟左右的视频
64 |
65 | ```
66 | ffmpeg -re -i test.flv -vcodec copy -acodec aac -ar 44100 -f flv rtmp://101.200.48.101:1935/stream/example
67 | ```
68 |
69 | 播放器输入这个地址就可以查看了:
70 |
71 | `rtmp://101.200.48.101:1935/stream/example`
72 |
73 | 比如, 截一张图:
74 |
75 | ```
76 | ffmpeg -i rtmp://101.200.48.101:1935/stream/example -frames:v 1 1.png
77 | ```
78 |
79 | 持续截图:
80 |
81 | ```
82 | ffmpeg -i rtmp://101.200.48.101:1935/stream/example -f image2 -r 1 -strftime 1 /tmp/%Y%m%d%H%M%S.jpg
83 | ```
84 |
85 | #### 调用函数
86 |
87 | ##### 1. 异步调用函数
88 |
89 | ```bash
90 | $ s invoke -e '{"rtmp_url" : "rtmp://101.200.48.101:1935/stream/example", "bucket":"my-bucket", "region":"cn-hangzhou", "dst":"dst"}' --invocation-type async
91 | ```
92 |
93 | 其中:
94 |
95 | - **rtmp_url:** 必需, 直播流地址
96 |
97 | - **bucket:** 必需, 保存截图文件的 bucket 名字
98 |
99 | - **region:** 可选,bucket 的 region, 不填默认使用函数所在的 region
100 |
101 | - **dst:** 可选,保存截图文件 bucket 的指定目录, 不填默认为空, 即根目录
102 |
103 | 发起推流后, 函数执行是根据推流是否结束, 推流结束了, 然后函数里面的 ffmpeg 截图命令也就结束了, 最后将 /tmp 下面的图片保存回 oss
104 |
105 |
106 | ##### 2. 登录[FC 控制台](https://fcnext.console.aliyun.com/),查看截图任务函数执行详情
107 |
108 | 
109 |
110 | ## 进阶
111 |
112 | 如果有边截图, 边上传回 oss 的需求, 可以基于这个代码再优化下
113 |
114 |
115 |
116 |
117 |
118 |
119 | ## 开发者社区
120 |
121 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
122 |
123 |
124 |
125 | |
|
|
|
126 | |--- | --- | --- |
127 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/rtmp-snapshot/src/s.yaml:
--------------------------------------------------------------------------------
1 | # ------------------------------------
2 | # 欢迎您使用阿里云函数计算 FC 组件进行项目开发
3 | # 组件仓库地址:https://github.com/devsapp/fc
4 | # 组件帮助文档:https://www.serverless-devs.com/fc/readme
5 | # Yaml参考文档:https://www.serverless-devs.com/fc/yaml/readme
6 | # 关于:
7 | # - Serverless Devs和FC组件的关系、如何声明/部署多个函数、超过50M的代码包如何部署
8 | # - 关于.fcignore使用方法、工具中.s目录是做什么、函数进行build操作之后如何处理build的产物
9 | # 等问题,可以参考文档:https://www.serverless-devs.com/fc/tips
10 | # 关于如何做CICD等问题,可以参考:https://www.serverless-devs.com/serverless-devs/cicd
11 | # 关于如何进行环境划分等问题,可以参考:https://www.serverless-devs.com/serverless-devs/extend
12 | # 更多函数计算案例,可参考:https://github.com/devsapp/awesome/
13 | # 有问题快来钉钉群问一下吧:33947367
14 | # ------------------------------------
15 |
16 | edition: 1.0.0
17 | name: video-transcode
18 | # access 是当前应用所需要的密钥信息配置:
19 | # 密钥配置可以参考:https://www.serverless-devs.com/serverless-devs/command/config
20 | # 密钥使用顺序可以参考:https://www.serverless-devs.com/serverless-devs/tool#密钥使用顺序与规范
21 | access: "{{ access }}"
22 |
23 | vars:
24 | region: "{{ region }}"
25 | service:
26 | name: "{{ serviceName }}"
27 | description: ffmpeg get rtmp stream snapshot
28 | internetAccess: true
29 | role: "{{ roleArn }}"
30 | # logConfig: auto
31 |
32 | services:
33 | rtmp-snapshot: # 业务名称/模块名称
34 | component: fc # 组件名称,Serverless Devs 工具本身类似于一种游戏机,不具备具体的业务能力,组件类似于游戏卡,用户通过向游戏机中插入不同的游戏卡实现不同的功能,即通过使用不同的组件实现不同的具体业务能力
35 | # actions: # 自定义执行逻辑,关于actions 的使用,可以参考:https://www.serverless-devs.com/serverless-devs/yaml#行为描述
36 | # pre-deploy: # 在deploy之前运行
37 | # - run: s version publish -a demo
38 | # path: ./src
39 | # - run: docker build xxx # 要执行的系统命令,类似于一种钩子的形式
40 | # path: ./src # 执行系统命令/钩子的路径
41 | # - plugin: myplugin # 与运行的插件 (可以通过s cli registry search --type Plugin 获取组件列表)
42 | # args: # 插件的参数信息
43 | # testKey: testValue
44 | props:
45 | region: ${vars.region}
46 | service: ${vars.service}
47 | function:
48 | name: snapshot
49 | runtime: python3
50 | Handler: index.handler
51 | codeUri: ./code/snapshot
52 | memorySize: 1536
53 | timeout: 7200
54 | environmentVariables:
55 | TZ: "{{ timeZone }}"
56 | asyncConfiguration:
57 | destination:
58 | # onSuccess: acs:fc:::services/${vars.service.name}/functions/dest-succ
59 | onFailure: acs:fc:::services/${vars.service.name}/functions/dest-fail
60 | maxAsyncEventAgeInSeconds: 18000
61 | maxAsyncRetryAttempts: 2
62 | statefulInvocation: true
63 |
64 | dest-succ: # 业务名称/模块名称
65 | component: fc
66 | props: # 组件的属性值
67 | region: ${vars.region}
68 | service: ${vars.service}
69 | function:
70 | name: dest-succ
71 | description: 'async task destination success function by serverless devs'
72 | runtime: python3
73 | codeUri: ./code/succ
74 | handler: index.handler
75 | memorySize: 512
76 | timeout: 60
77 |
78 | dest-fail: # 业务名称/模块名称
79 | component: fc
80 | props: # 组件的属性值
81 | region: ${vars.region}
82 | service: ${vars.service}
83 | function:
84 | name: dest-fail
85 | description: 'async task destination fail function by serverless devs'
86 | runtime: python3
87 | codeUri: ./code/fail
88 | handler: index.handler
89 | memorySize: 512
90 | timeout: 60
91 |
92 | # next-function: # 第二个函数的案例,仅供参考
93 | # # 如果在当前项目下执行 s deploy,会同时部署模块:
94 | # # helloworld:服务hello-world-service,函数cpp-event-function
95 | # # next-function:服务hello-world-service,函数next-function-example
96 | # # 如果想单独部署当前服务与函数,可以执行 s + 模块名/业务名 + deploy,例如:s next-function deploy
97 | # # 如果想单独部署当前函数,可以执行 s + 模块名/业务名 + deploy function,例如:s next-function deploy function
98 | # # 更多命令可参考:https://www.serverless-devs.com/fc/readme#文档相关
99 | # component: fc
100 | # props:
101 | # region: ${vars.region}
102 | # service: ${vars.service} # 应用整体的服务配置
103 | # function: # 定义一个新的函数
104 | # name: next-function-example
105 | # description: 'hello world by serverless devs'
106 |
--------------------------------------------------------------------------------
/rtmp-snapshot/version.md:
--------------------------------------------------------------------------------
1 | - 第一版
2 |
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | .s
3 |
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/hook/index.js:
--------------------------------------------------------------------------------
1 | async function preInit(inputObj) {
2 | console.log(`\n _______ _______ __ __ _______ _______ _______
3 | | || || |_| || || || |
4 | | ___|| ___|| || _ || ___|| ___|
5 | | |___ | |___ | || |_| || |___ | | __
6 | | ___|| ___|| || ___|| ___|| || |
7 | | | | | | ||_|| || | | |___ | |_| |
8 | |___| |___| |_| |_||___| |_______||_______|
9 | `)
10 | }
11 |
12 | async function postInit(inputObj) {
13 | console.log(`\n Welcome to the ffmpeg-app application
14 | This application requires to open these services:
15 | FC : https://fc.console.aliyun.com/
16 | OSS: https://oss.console.aliyun.com/
17 | ACR: https://cr.console.aliyun.com/
18 |
19 | * 关于项目的介绍,可以参考:https://github.com/devsapp/start-ffmpeg/blob/master/headless-ffmpeg/src
20 | * 项目初始化完成,您可以直接进入项目目录下
21 | 1. 对s.yaml进行升级,例如填充好environmentVariables中的部分变量值(OSS存储桶相关信息)
22 | 2. 开通容器镜像服务,并创建相关的实例、命名空间,并将内容对应填写到image字段中
23 | 3. 进行构建:s build --use-docker --dockerfile ./code/Dockerfile
24 | 4. 项目部署:s deploy --use-local -y
25 | * 最后您还可以验证项目的正确性,例如通过invoke调用(这里video_url等,可以考虑换成自己的测试mp4):
26 | s invoke -e '{"record_time":"35","video_url":"https://dy-vedio.oss-cn-hangzhou.aliyuncs.com/video/a.mp4","output_file":"record/test.mp4"}'
27 | \n`)
28 | }
29 |
30 | module.exports = {
31 | postInit,
32 | preInit
33 | }
34 |
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/publish.yaml:
--------------------------------------------------------------------------------
1 | Type: Application
2 | Name: Ffmpeg-online
3 | Provider:
4 | - 阿里云
5 | Version: 0.0.8
6 | Description: 快速部署一个ffmpeg-online应用到阿里云函数计算
7 | HomePage: https://github.com/devsapp/start-ffmpeg/tree/master/serverless-ffmpeg-online
8 | Tags:
9 | - 视频在线处理
10 | Category: 音视频处理
11 | Service:
12 | 函数计算:
13 | Authorities:
14 | - AliyunFCFullAccess
15 | - AliyunContainerRegistryFullAccess
16 | Parameters:
17 | type: object
18 | additionalProperties: false # 不允许增加其他属性
19 | required: # 必填项
20 | - region
21 | - serviceName
22 | - functionName
23 | - roleArn
24 | - acrRegistry
25 | - ossBucket
26 | - timeZone
27 | properties:
28 | region:
29 | title: 地域
30 | type: string
31 | default: cn-hangzhou
32 | description: 创建应用所在的地区
33 | enum:
34 | - cn-beijing
35 | - cn-hangzhou
36 | - cn-shanghai
37 | - cn-qingdao
38 | - cn-zhangjiakou
39 | - cn-huhehaote
40 | - cn-shenzhen
41 | - cn-chengdu
42 | - cn-hongkong
43 | - ap-southeast-1
44 | - ap-southeast-2
45 | - ap-southeast-3
46 | - ap-southeast-5
47 | - ap-northeast-1
48 | - eu-central-1
49 | - eu-west-1
50 | - us-west-1
51 | - us-east-1
52 | - ap-south-1
53 | serviceName:
54 | title: 服务名
55 | type: string
56 | default: ffmpeg_online-${default-suffix}
57 | pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$"
58 | description: 服务名称,只能包含字母、数字、下划线和中划线。不能以数字、中划线开头。长度在 1-128 之间
59 | functionName:
60 | title: 函数名
61 | type: string
62 | default: ffmpeg_online
63 | description: 函数名称,只能包含字母、数字、下划线和中划线。不能以数字、中划线开头。长度在 1-64 之间
64 | roleArn:
65 | title: RAM角色ARN
66 | type: string
67 | default: ""
68 | pattern: "^acs:ram::[0-9]*:role/.*$"
69 | description: "函数计算访问其他云服务时使用的服务角色,需要填写具体的角色ARN,格式为acs:ram::$account-id>:role/$role-name。例如:acs:ram::14310000000:role/aliyunfcdefaultrole。
70 | \n如果您没有特殊要求,可以使用函数计算提供的默认的服务角色,即AliyunFCDefaultRole, 并增加 AliyunContainerRegistryFullAccess 权限。如果您首次使用函数计算,可以访问 https://fcnext.console.aliyun.com 进行授权。
71 | \n详细文档参考 https://help.aliyun.com/document_detail/181589.html#section-o93-dbr-z6o"
72 | required: true
73 | x-role:
74 | name: fcacrrole
75 | service: fc
76 | authorities:
77 | - AliyunContainerRegistryFullAccess
78 | - AliyunFCDefaultRolePolicy
79 | acrRegistry:
80 | title: 阿里云容器镜像
81 | type: string
82 | examples: ["registry.cn-hangzhou.aliyuncs.com/fc-demo/headless-ffmpeg:v1"]
83 | description: 阿里云容器镜像服务 image 的名字
84 | x-acr:
85 | type: "select"
86 | ossBucket:
87 | title: OSS 存储桶名
88 | type: string
89 | default: ""
90 | description: OSS 存储桶名
91 | x-bucket:
92 | dependency:
93 | - region
94 | timeZone:
95 | title: 时区
96 | type: string
97 | default: Asia/Shanghai
98 | description: 创建的应用函数执行时候所在实例的时区, 详情参考 https://docs.oracle.com/middleware/12211/wcs/tag-ref/MISC/TimeZones.html
99 |
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/readme.md:
--------------------------------------------------------------------------------
1 | # Ffmpeg online 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署一个在线ffmpeg应用到阿里云函数计算***
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## 部署 & 体验
32 |
33 |
34 |
35 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=PanoramicPageRecording) ,
36 | [](https://fcnext.console.aliyun.com/applications/create?template=PanoramicPageRecording)
37 | 该应用。
38 |
39 |
40 |
41 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
42 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install)
43 | ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
44 | - 初始化项目:`s init PanoramicPageRecording -d PanoramicPageRecording`
45 | - 进入项目,并进行项目部署:`cd PanoramicPageRecording && s deploy -y`
46 |
47 |
48 |
49 |
50 |
51 | # 调用函数
52 |
53 | ``` bash
54 | # deploy
55 | $ s deploy -y --use-local
56 | # Invoke
57 | $ s invoke -e '{"input_file":"record/test.mp4","video_format":"mp4","start_time":"10","duration":"16","output_file":"record/result.mp4","commands":["-r 30","-f avi"]}'
58 | ```
59 |
60 | 调用成功后, 会在对应的 bucket 下, 产生 record/result.mp4 大约 16 秒的每秒 30 帧的处理后的视频。
61 |
62 | # 入参示例
63 |
64 | ```JSON
65 | {
66 | "input_file": "path/to/your/video.mp4",
67 | "output_file": "path/to/save/processed.mp4",
68 | "video_format": "mp4",
69 | "start_time": "10",
70 | "duration": "16",
71 |
72 | "commands": [
73 | "-r 25",
74 | "-f mp4"
75 | ]
76 | }
77 |
78 | ```
79 |
80 | ## 参数说明
81 | 参数分为两部分,一部分是应用支持到参数,具体包括:
82 | - video_format
83 | - codec
84 | - video_bit_rate
85 | - audio_bit_rate
86 | - video_frame_rate
87 | - duration
88 | - video_aspect_ratio
89 | - start_time
90 | - video_size
91 | - audio_codec
92 | - audio_frequency
93 | - audio_quality
94 | 如果上述参数不能满足您的视频处理需求,可自行在commands中传入
95 | 处理字符传,范式为'-option param',示例如同'-r 30'
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | ```bash
104 | # 如果有镜像有代码更新, 重新build 镜像
105 | $ docker build -t ffmpeg-online -f ./code/Dockerfile ./code
106 | ```
107 |
108 | # 原理
109 |
110 | 将欲处理的视频下载到custom container载通过底层调用ffmpeg进行处理,最后上传到OSS
111 |
112 | [PushObjectCache](https://next.api.aliyun.com/api/Cdn/2018-05-10/PushObjectCache?lang=NODEJS&sdkStyle=old¶ms={})
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | ## 开发者社区
121 |
122 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues)
123 | 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
124 |
125 |
126 |
127 | |
|
|
|
128 | |--- | --- | --- |
129 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/src/.gitignore:
--------------------------------------------------------------------------------
1 | /headless-ffmpeg
2 |
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/src/code/Dockerfile:
--------------------------------------------------------------------------------
1 | # https://hub.docker.com/r/aliyunfc/headless-ffmpeg
2 | FROM aliyunfc/headless-ffmpeg
3 |
4 | # set time zone (current is Shanghai, China)
5 | ENV TZ=Asia/Shanghai
6 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
7 | RUN dpkg-reconfigure -f noninteractive tzdata
8 |
9 | ENV LANG zh_CN.UTF-8
10 | ENV LANGUAGE zh_CN:zh
11 | ENV LC_ALL zh_CN.UTF-8
12 | # set Xvfb auth file
13 | ENV XAUTHORITY=/tmp/Xauthority
14 |
15 | WORKDIR /code
16 |
17 | ENV PUPPETEER_SKIP_DOWNLOAD=true
18 | RUN npm install express \
19 | ali-oss \
20 | fluent-ffmpeg \
21 | --registry http://registry.npm.taobao.org
22 |
23 | COPY ./control.js ./control.js
24 |
25 | RUN mkdir -p /var/output
26 |
27 | EXPOSE 9000
28 |
29 | ENTRYPOINT ["node", "control.js"]
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/src/code/control.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Constants
4 | const PORT = 9000;
5 | const HOST = '0.0.0.0';
6 | const REQUEST_ID_HEADER = 'x-fc-request-id'
7 | const ACCESS_KEY_ID = 'x-fc-access-key-id'
8 | const ACCESS_KEY_SECRET = 'x-fc-access-key-secret'
9 | const SECURITY_TOKEN = 'x-fc-security-token'
10 |
11 | var execSync = require("child_process").execSync;
12 | var ffmpeg = require('fluent-ffmpeg');
13 | const OSS = require('ali-oss');
14 | const express = require('express');
15 | const app = express();
16 | app.use(express.json())
17 |
18 | // invocation
19 | app.post('/', async (req, res) => {
20 | var rid = req.headers[REQUEST_ID_HEADER]
21 | console.log(`FC Invoke Start RequestId: ${rid}`)
22 | try {
23 | // Prior to get process parameters from request body to do your things
24 | console.log(JSON.stringify(req.body));
25 | var processParams = req.body
26 | // Make input_file parameter as the inidcator
27 | if (!processParams["input_file"]) {
28 | console.log("Miss mandotary video processing parameters in request body, try to get the parameters from query")
29 | // Fallback: Try to get recording parameters from query to do your things
30 | console.log(JSON.stringify(req.query));
31 | processParams = req.query
32 | }
33 |
34 | // Compatible with old event mode
35 | // var processParams = processParams
36 | if (!processParams["input_file"]) {
37 | res.status(400).send("Miss mandotary video processing parameters");
38 | console.log(`FC Invoke End RequestId: ${rid}`)
39 | return
40 | }
41 |
42 | // 连接OSS
43 | const client = new OSS({
44 | accessKeyId: req.headers[ACCESS_KEY_ID],
45 | accessKeySecret: req.headers[ACCESS_KEY_SECRET],
46 | stsToken: req.headers[SECURITY_TOKEN],
47 | bucket: process.env.OSS_BUCKET,
48 | endpoint: process.env.OSS_ENDPOINT,
49 | });
50 |
51 |
52 | await downloadFile(client, processParams["input_file"], 'origin.mp4')
53 | let video = ffmpeg('origin.mp4')
54 | await processFile(video, processParams)
55 | await uploadFile(client, 'result.mp4', processParams["output_file"])
56 | res.send('OK')
57 |
58 | } catch (e) {
59 | res.status(404).send(e.stack || e);
60 | console.log(`FC Invoke End RequestId: ${rid}, Error: Unhandled function error`)
61 | }
62 | });
63 |
64 | var server = app.listen(PORT, HOST);
65 | console.log(`Running on http://${HOST}:${PORT}`);
66 |
67 | server.timeout = 0; // never timeout
68 | server.keepAliveTimeout = 0; // keepalive, never timeout
69 |
70 | async function processFile(video, processParams) {
71 | console.log("尝试处理视频--v25")
72 | return new Promise((resolve, reject) => {
73 |
74 | try {
75 | processVideo(video, processParams)
76 | } catch (e) {
77 | console.log("保存参数出错")
78 | }
79 |
80 | video
81 | .on('start', function () {
82 | console.log('转换任务开始~')
83 | })
84 | .on('progress', function (progress) {
85 | console.log('进行中,完成' + progress.percent + '%')
86 | })
87 | .on('error', function (err, stdout, stderr) {
88 | console.log('Cannot process video: ' + err.message)
89 | reject(err)
90 | })
91 | .on('end', function (str) {
92 | console.log('转换任务完成!')
93 | resolve()
94 | })
95 | .save('result.mp4')
96 | });
97 | }
98 |
99 | async function downloadFile(client, ossFilePath, savePath) {
100 | console.log("尝试下载文件:" + ossFilePath)
101 |
102 | try {
103 | const result = await client.get(ossFilePath, savePath);
104 | await execSync("ls -lht", {stdio: 'inherit'});
105 | } catch (e) {
106 | console.log("下载视频失败")
107 | console.log(e)
108 | }
109 |
110 | }
111 |
112 | async function uploadFile(client, localPath, ossFilePath) {
113 | console.log("尝试上传文件为:" + ossFilePath)
114 |
115 | try {
116 | const result = await client.put(ossFilePath, localPath)
117 | console.log(result)
118 | } catch (e) {
119 | console.log("上传视频失败")
120 | console.log(e)
121 | }
122 | }
123 |
124 |
125 | function processVideo(video, processParams) {
126 | if (processParams["video_format"] != null) {
127 | video.format(processParams["video_format"])
128 | } else {
129 | video.format('mp4')
130 | }
131 | if (processParams["codec"] != null) {
132 | video.videoCodec(processParams["codec"])
133 | }
134 | if (processParams["video_bit_rate"] != null) {
135 | video.videoBitrate(processParams["video_bit_rate"])
136 | }
137 | if (processParams["audio_bit_rate"] != null) {
138 | video.audioBitrate(processParams["audio_bit_rate"])
139 | }
140 | if (processParams["video_frame_rate"] != null) {
141 | video.fps(processParams["video_frame_rate"])
142 | }
143 | if (processParams["duration"] != null) {
144 | video.duration(processParams["duration"])
145 | }
146 | if (processParams["video_aspect_ratio"] != null) {
147 | video.aspect(processParams["video_aspect_ratio"])
148 | }
149 | if (processParams["start_time"] != null) {
150 | video.setStartTime(processParams["start_time"])
151 | }
152 | if (processParams["video_size"] != null) {
153 | video.videoSize(processParams["video_size"])
154 | }
155 | if (processParams["audio_codec"] != null) {
156 | video.audioCodec(processParams["audio_codec"])
157 | }
158 | if (processParams["audio_frequency"] != null) {
159 | video.audioFrequency(processParams["audio_frequency"])
160 | }
161 |
162 | if (processParams["audio_quality"] != null) {
163 | video.audioQuality(processParams["audio_quality"])
164 | }
165 |
166 | if (processParams["commands"] != null) {
167 | let additionalCommands = processParams["commands"]
168 | console.log(additionalCommands)
169 | for (let i in additionalCommands) {
170 | video.addOption(additionalCommands[i])
171 | }
172 | }
173 |
174 | }
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/src/dest/fail/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation fail: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/src/dest/succ/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation success: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/src/readme.md:
--------------------------------------------------------------------------------
1 | # Ffmpeg online 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署一个在线ffmpeg应用到阿里云函数计算***
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## 部署 & 体验
32 |
33 |
34 |
35 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=PanoramicPageRecording) ,
36 | [](https://fcnext.console.aliyun.com/applications/create?template=PanoramicPageRecording)
37 | 该应用。
38 |
39 |
40 |
41 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
42 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install)
43 | ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
44 | - 初始化项目:`s init PanoramicPageRecording -d PanoramicPageRecording`
45 | - 进入项目,并进行项目部署:`cd PanoramicPageRecording && s deploy -y`
46 |
47 |
48 |
49 |
50 |
51 | # 调用函数
52 |
53 | ``` bash
54 | # deploy
55 | $ s deploy -y --use-local
56 | # Invoke
57 | $ s invoke -e '{"input_file":"record/test.mp4","video_format":"mp4","start_time":"10","duration":"16","output_file":"record/result.mp4","commands":["-r 30","-f avi"]}'
58 | ```
59 |
60 | 调用成功后, 会在对应的 bucket 下, 产生 record/result.mp4 大约 16 秒的每秒 30 帧的处理后的视频。
61 |
62 | # 入参示例
63 |
64 | ```JSON
65 | {
66 | "input_file": "path/to/your/video.mp4",
67 | "output_file": "path/to/save/processed.mp4",
68 | "video_format": "mp4",
69 | "start_time": "10",
70 | "duration": "16",
71 |
72 | "commands": [
73 | "-r 25",
74 | "-f mp4"
75 | ]
76 | }
77 |
78 | ```
79 |
80 | ## 参数说明
81 | 参数分为两部分,一部分是应用支持到参数,具体包括:
82 | - video_format
83 | - codec
84 | - video_bit_rate
85 | - audio_bit_rate
86 | - video_frame_rate
87 | - duration
88 | - video_aspect_ratio
89 | - start_time
90 | - video_size
91 | - audio_codec
92 | - audio_frequency
93 | - audio_quality
94 | 如果上述参数不能满足您的视频处理需求,可自行在commands中传入
95 | 处理字符传,范式为'-option param',示例如同'-r 30'
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | ```bash
104 | # 如果有镜像有代码更新, 重新build 镜像
105 | $ docker build -t ffmpeg-online -f ./code/Dockerfile ./code
106 | ```
107 |
108 | # 原理
109 |
110 | 将欲处理的视频下载到custom container载通过底层调用ffmpeg进行处理,最后上传到OSS
111 |
112 | [PushObjectCache](https://next.api.aliyun.com/api/Cdn/2018-05-10/PushObjectCache?lang=NODEJS&sdkStyle=old¶ms={})
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | ## 开发者社区
121 |
122 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues)
123 | 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
124 |
125 |
126 |
127 | |
|
|
|
128 | |--- | --- | --- |
129 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/src/s.yaml:
--------------------------------------------------------------------------------
1 | # ------------------------------------
2 | # 欢迎您使用阿里云函数计算 FC 组件进行项目开发
3 | # 组件仓库地址:https://github.com/devsapp/fc
4 | # 组件帮助文档:https://www.serverless-devs.com/fc/readme
5 | # Yaml参考文档:https://www.serverless-devs.com/fc/yaml/readme
6 | # 关于:
7 | # - Serverless Devs和FC组件的关系、如何声明/部署多个函数、超过50M的代码包如何部署
8 | # - 关于.fcignore使用方法、工具中.s目录是做什么、函数进行build操作之后如何处理build的产物
9 | # 等问题,可以参考文档:https://www.serverless-devs.com/fc/tips
10 | # 关于如何做CICD等问题,可以参考:https://www.serverless-devs.com/serverless-devs/cicd
11 | # 关于如何进行环境划分等问题,可以参考:https://www.serverless-devs.com/serverless-devs/extend
12 | # 更多函数计算案例,可参考:https://github.com/devsapp/awesome/
13 | # 有问题快来钉钉群问一下吧:33947367
14 | # ------------------------------------
15 |
16 | edition: 1.0.0
17 | name: ffmpeg-online
18 | # access 是当前应用所需要的密钥信息配置:
19 | # 密钥配置可以参考:https://www.serverless-devs.com/serverless-devs/command/config
20 | # 密钥使用顺序可以参考:https://www.serverless-devs.com/serverless-devs/tool#密钥使用顺序与规范
21 | access: "{{ access }}"
22 |
23 | vars: # 全局变量
24 | region: "{{ region }}"
25 | service:
26 | name: "{{ serviceName }}"
27 | role: "{{ roleArn }}"
28 | description: 'Record a video for chrome browser'
29 | internetAccess: true
30 | functionName: "{{ functionName }}"
31 |
32 | services:
33 | ffmpeg_online_project: # 业务名称/模块名称
34 | component: fc # 组件名称,Serverless Devs 工具本身类似于一种游戏机,不具备具体的业务能力,组件类似于游戏卡,用户通过向游戏机中插入不同的游戏卡实现不同的功能,即通过使用不同的组件实现不同的具体业务能力
35 | actions:
36 | pre-deploy:
37 | - component: fc build --use-docker --dockerfile ./code/Dockerfile
38 | post-deploy:
39 | - component: fc api UpdateFunction --region ${vars.region} --header '{"x-fc-disable-container-reuse":"True"}' --path '{"serviceName":"${vars.service.name}","functionName":"${vars.functionName}"}'
40 | props:
41 | region: ${vars.region}
42 | service: ${vars.service}
43 | function:
44 | name: ${vars.functionName}
45 | runtime: custom-container
46 | memorySize: 8192
47 | instanceType: c1
48 | timeout: 7200
49 | customContainerConfig:
50 | image: "{{ acrRegistry }}"
51 | environmentVariables:
52 | OSS_BUCKET: "{{ ossBucket }}"
53 | OSS_ENDPOINT: oss-${vars.region}-internal.aliyuncs.com
54 | TZ: "{{ timeZone }}"
55 | asyncConfiguration:
56 | destination:
57 | # onSuccess: acs:fc:::services/${vars.service.name}/functions/dest-succ
58 | onFailure: acs:fc:::services/${vars.service.name}/functions/dest-fail
59 | maxAsyncEventAgeInSeconds: 18000
60 | maxAsyncRetryAttempts: 0
61 | statefulInvocation: true
62 | triggers:
63 | - name: httpTrigger
64 | type: http
65 | config:
66 | authType: anonymous
67 | methods:
68 | - POST
69 |
70 | dest-succ: # 业务名称/模块名称
71 | component: fc
72 | props: # 组件的属性值
73 | region: ${vars.region}
74 | service: ${vars.service}
75 | function:
76 | name: dest-succ
77 | description: 'async task destination success function by serverless devs'
78 | runtime: python3
79 | codeUri: ./dest/succ
80 | handler: index.handler
81 | memorySize: 512
82 | timeout: 60
83 |
84 | dest-fail: # 业务名称/模块名称
85 | component: fc
86 | props: # 组件的属性值
87 | region: ${vars.region}
88 | service: ${vars.service}
89 | function:
90 | name: dest-fail
91 | description: 'async task destination fail function by serverless devs'
92 | runtime: python3
93 | codeUri: ./dest/fail
94 | handler: index.handler
95 | memorySize: 512
96 | timeout: 60
--------------------------------------------------------------------------------
/serverless-ffmpeg-online/version.md:
--------------------------------------------------------------------------------
1 | - 第一版
2 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | .s
3 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/hook/index.js:
--------------------------------------------------------------------------------
1 | async function preInit(inputObj) {
2 | console.log(`\n _______ _______ __ __ _______ _______ _______
3 | | || || |_| || || || |
4 | | ___|| ___|| || _ || ___|| ___|
5 | | |___ | |___ | || |_| || |___ | | __
6 | | ___|| ___|| || ___|| ___|| || |
7 | | | | | | ||_|| || | | |___ | |_| |
8 | |___| |___| |_| |_||___| |_______||_______|
9 | `)
10 | }
11 |
12 | async function postInit(inputObj) {
13 | console.log(`\n Welcome to the ffmpeg-app application
14 | This application requires to open these services:
15 | FC : https://fc.console.aliyun.com/
16 | OSS: https://oss.console.aliyun.com/
17 | ACR: https://cr.console.aliyun.com/
18 |
19 | * 关于项目的介绍,可以参考:https://github.com/devsapp/start-ffmpeg/blob/master/headless-ffmpeg/src
20 | * 项目初始化完成,您可以直接进入项目目录下
21 | 1. 对s.yaml进行升级,例如填充好environmentVariables中的部分变量值(OSS存储桶相关信息)
22 | 2. 开通容器镜像服务,并创建相关的实例、命名空间,并将内容对应填写到image字段中
23 | 3. 进行构建:s build --use-docker --dockerfile ./code/Dockerfile
24 | 4. 项目部署:s deploy --use-local -y
25 | * 最后您还可以验证项目的正确性,例如通过invoke调用(这里video_url等,可以考虑换成自己的测试mp4):
26 | s invoke -e '{"record_time":"35","video_url":"https://dy-vedio.oss-cn-hangzhou.aliyuncs.com/video/a.mp4","output_file":"record/test.mp4"}'
27 | \n`)
28 | }
29 |
30 | module.exports = {
31 | postInit,
32 | preInit
33 | }
34 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/publish.yaml:
--------------------------------------------------------------------------------
1 | Type: Application
2 | Name: HttpPanoramicPageRecording
3 | Provider:
4 | - 阿里云
5 | Version: 0.0.25
6 | Description: 快速部署一个Http触发的全景web页面录制-高级版应用到阿里云函数计算
7 | 相对于"全景录制",本应用的新增feature主要有:
8 | 1.页面不显示鼠标,提升观看体验
9 | 2.无需手动传入OSS的key&secret
10 | 3.支持传入帧率,支持推流
11 | 4.优化了前期启动时间及后期结束时间
12 | 5.修改了触发方式,该版本为http触发
13 | HomePage: https://github.com/devsapp/start-ffmpeg/tree/master/serverless-panoramic-page-recording-http
14 | Tags:
15 | - 全景录制
16 | Category: 音视频处理
17 | Service:
18 | 函数计算:
19 | Authorities:
20 | - AliyunFCFullAccess
21 | - AliyunContainerRegistryFullAccess
22 | Parameters:
23 | type: object
24 | additionalProperties: false # 不允许增加其他属性
25 | required: # 必填项
26 | - region
27 | - serviceName
28 | - functionName
29 | - roleArn
30 | - acrImage
31 | - ossBucket
32 | - timeZone
33 | properties:
34 | region:
35 | title: 地域
36 | type: string
37 | default: cn-hangzhou
38 | description: 创建应用所在的地区
39 | enum:
40 | - cn-beijing
41 | - cn-hangzhou
42 | - cn-shanghai
43 | - cn-qingdao
44 | - cn-zhangjiakou
45 | - cn-huhehaote
46 | - cn-shenzhen
47 | - cn-chengdu
48 | - cn-hongkong
49 | - ap-southeast-1
50 | - ap-southeast-2
51 | - ap-southeast-3
52 | - ap-southeast-5
53 | - ap-northeast-1
54 | - eu-central-1
55 | - eu-west-1
56 | - us-west-1
57 | - us-east-1
58 | - ap-south-1
59 | serviceName:
60 | title: 服务名
61 | type: string
62 | default: browser_video_recorder-${default-suffix}
63 | pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$"
64 | description: 服务名称,只能包含字母、数字、下划线和中划线。不能以数字、中划线开头。长度在 1-128 之间
65 | functionName:
66 | title: 函数名
67 | type: string
68 | default: recoder
69 | description: 函数名称,只能包含字母、数字、下划线和中划线。不能以数字、中划线开头。长度在 1-64 之间
70 | roleArn:
71 | title: RAM角色ARN
72 | type: string
73 | default: ""
74 | pattern: "^acs:ram::[0-9]*:role/.*$"
75 | description: "函数计算访问其他云服务时使用的服务角色,需要填写具体的角色ARN,格式为acs:ram::$account-id>:role/$role-name。例如:acs:ram::14310000000:role/aliyunfcdefaultrole。
76 | \n如果您没有特殊要求,可以使用函数计算提供的默认的服务角色,即AliyunFCDefaultRole, 并增加 AliyunContainerRegistryFullAccess 权限。如果您首次使用函数计算,可以访问 https://fcnext.console.aliyun.com 进行授权。
77 | \n详细文档参考 https://help.aliyun.com/document_detail/181589.html#section-o93-dbr-z6o"
78 | required: true
79 | x-role:
80 | name: fcacrrole
81 | service: fc
82 | authorities:
83 | - AliyunContainerRegistryFullAccess
84 | - AliyunFCDefaultRolePolicy
85 | acrRegistry:
86 | title: 阿里云容器镜像
87 | type: string
88 | examples: ["registry.cn-hangzhou.aliyuncs.com/fc-demo/headless-ffmpeg:v1"]
89 | description: 阿里云容器镜像服务 image 的名字
90 | x-acr:
91 | type: "select"
92 | ossBucket:
93 | title: OSS 存储桶名
94 | type: string
95 | default: ""
96 | description: OSS 存储桶名
97 | x-bucket:
98 | dependency:
99 | - region
100 | timeZone:
101 | title: 时区
102 | type: string
103 | default: Asia/Shanghai
104 | description: 创建的应用函数执行时候所在实例的时区, 详情参考 https://docs.oracle.com/middleware/12211/wcs/tag-ref/MISC/TimeZones.html
105 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/readme.md:
--------------------------------------------------------------------------------
1 | # HttpPanoramicPageRecording 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署一个全景录制的应用到阿里云函数计算***
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## 部署 & 体验
32 |
33 |
34 |
35 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=HttpPanoramicPageRecording&type=direct) ,
36 | [](https://fcnext.console.aliyun.com/applications/create?template=HttpPanoramicPageRecording&type=direct) 该应用。
37 |
38 |
39 |
40 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
41 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
42 | - 初始化项目:`s init HttpPanoramicPageRecording -d HttpPanoramicPageRecording`
43 | - 进入项目,并进行项目部署:`cd HttpPanoramicPageRecording && s deploy -y`
44 |
45 |
46 |
47 |
48 |
49 | # 调用函数
50 |
51 | ``` bash
52 | # deploy
53 | $ s deploy -y --use-local
54 | # Invoke
55 | $ s invoke -e '{"record_time":"60","video_url":"https://tv.cctv.com/live/cctv1/","output_file":"record/test.mp4", "width":"1920", "height":"1080", "scale": 0.75, "frame_rate":25,"bit_rate":"2000k"}'
56 | ```
57 |
58 | 调用成功后, 会在对应的 bucket 下, 产生 record/test.mp4 大约 60 秒 1920x1080 的全景录制视频。
59 |
60 | 其中参数的意义:
61 |
62 | **1.record_time:** 录制时长
63 |
64 | **2.video_url:** 录制视频的 url
65 |
66 | **3.width:** 录制视频的宽度
67 |
68 | **4.height:** 录制视频的高度
69 |
70 | **5.scale:** 浏览器缩放比例
71 |
72 | **6.output_file:** 最后录制视频保存的 OSS 目录
73 |
74 | **7.frame_rate:** 录制视频的帧率(可不传递,默认帧率为30fps)
75 |
76 | **8.bit_rate:** 录制视频的码率(可不传递,默认码率为1500k)
77 |
78 | **9.output_stream:** 推流地址(可选参数,eg: rtmp://demo.aliyundoc.com/app/stream?xxxx)
79 |
80 | 其中 scale 是对浏览器进行 75% 的缩放,使视频能录制更多的网页内容
81 |
82 | **注意:** 如果您录制的视频存在一些卡顿或者快进, 可能是因为您录制的视频分辨率大并且复杂, 消耗的 CPU 很大, 您可以通过调大函数的规格, 提高 CPU 的能力。
83 |
84 | 比如上面的示例参数得到下图:
85 |
86 | 
87 |
88 | # 如何本地调试
89 |
90 | 直接本地运行, 命令执行完毕后, 会在当前目录生成一个 test.mp4 的视频
91 |
92 | ```bash
93 | # 直接本地执行docker命令, 会在当前目录生成一个 test.mp4 的视频
94 | $ docker run --rm --entrypoint="" -v $(pwd):/var/output aliyunfc/browser_recorder /code/record.sh 60 https://tv.cctv.com/live/cctv1 1920x1080x24 1920,1080 1920x1080 1 25 2000k
95 | ```
96 |
97 | 调试
98 |
99 | ```bash
100 | # 如果有镜像有代码更新, 重新build 镜像
101 | $ docker build -t my-panoramic-page-recording -f ./code/Dockerfile ./code
102 | # 测试全屏录制核心脚本 record.sh, 执行完毕后, 会在当前目录有一个 test.mp4 的视频
103 | $ docker run --rm --entrypoint="" -v $(pwd):/var/output my-panoramic-page-recording /code/record.sh 60 https://tv.cctv.com/live/cctv1 1920x1080x24 1920,1080 1920x1080 1 25 2000k
104 | ```
105 |
106 | > 其中 record.sh 的参数意义:
107 | >
108 | > 1. 录制时长
109 | > 2. 视频 url
110 | > 3. $widthx$heightx24
111 | > 4. $width,$height
112 | > 5. $widthx$height
113 | > 6. chrome 浏览器缩放比例
114 | > 7. 帧率
115 | > 8. 码率
116 | > 9. 推流地址
117 |
118 | # 原理
119 |
120 | Chrome 渲染到虚拟 X-server,并通过 FFmpeg 抓取系统桌⾯,通过启动 xvfb 启动虚拟 X-server,Chrome 进⾏全屏显示渲染到到虚拟 X-server 上,并通过 FFmpeg 抓取系统屏幕以及采集系统声⾳并进⾏编码写⽂件。这种⽅式的适配性⾮常好, 不仅可以录制 Chrome,理论上也可以录制其他的应⽤。缺点是占⽤的内存和 CPU 较多。
121 |
122 | **server.js**
123 |
124 | custom container http server 逻辑
125 |
126 | **record.sh**
127 |
128 | 核心录屏逻辑, 启动 xvfb, 在虚拟 X-server 中使用 `record.js` 中的 puppeteer 启动浏览器, 最后 FFmpeg 完成 X-server 屏幕的视频和音频抓取工作, 生成全屏录制后的视频
129 |
130 | # 其他
131 |
132 | 如果您想将生成的视频直接预热的 CDN, 以阿里云 CDN 为例, 只需要在 server.js 上传完 OSS bucket 后的逻辑中增加如下代码:
133 |
134 | [PushObjectCache](https://next.api.aliyun.com/api/Cdn/2018-05-10/PushObjectCache?lang=NODEJS&sdkStyle=old¶ms={})
135 |
136 | > Tips 前提需要配置好 CDN
137 |
138 |
139 |
140 |
141 |
142 | ## 开发者社区
143 |
144 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
145 |
146 |
147 |
148 | |
|
|
|
149 | |--- | --- | --- |
150 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/src/.gitignore:
--------------------------------------------------------------------------------
1 | /headless-ffmpeg
2 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/src/code/Dockerfile:
--------------------------------------------------------------------------------
1 | # https://hub.docker.com/r/aliyunfc/headless-ffmpeg
2 | FROM aliyunfc/headless-ffmpeg
3 |
4 | # set time zone (current is Shanghai, China)
5 | ENV TZ=Asia/Shanghai
6 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
7 | RUN dpkg-reconfigure -f noninteractive tzdata
8 |
9 | ENV LANG zh_CN.UTF-8
10 | ENV LANGUAGE zh_CN:zh
11 | ENV LC_ALL zh_CN.UTF-8
12 | # set Xvfb auth file
13 | ENV XAUTHORITY=/tmp/Xauthority
14 |
15 | WORKDIR /code
16 |
17 | ENV PUPPETEER_SKIP_DOWNLOAD=true
18 | RUN npm install puppeteer-core \
19 | express \
20 | ali-oss \
21 | fs \
22 | --registry http://registry.npm.taobao.org
23 |
24 | COPY ./record.sh ./record.sh
25 | COPY ./record.js ./record.js
26 | COPY ./server.js ./server.js
27 |
28 | RUN mkdir -p /var/output
29 |
30 | EXPOSE 9000
31 |
32 | ENTRYPOINT ["node", "server.js"]
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/src/code/record.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer-core');
2 | const fs = require('fs');
3 |
4 | var args = process.argv.splice(2)
5 | console.log(args);
6 |
7 | const scale_factor = parseFloat(args[3], 10)
8 | const li = args[2].split(',');
9 | console.log(li)
10 | const w = parseInt(li[0], 10)
11 | const h = parseInt(li[1], 10)
12 |
13 | async function record() {
14 |
15 | var win_size = `${Math.floor(w / scale_factor)},${Math.floor(h / scale_factor)}`
16 | const browser = await puppeteer.launch(
17 | {
18 | headless: false,
19 | executablePath: "/usr/bin/google-chrome-stable",
20 | args: [
21 | '--no-sandbox',
22 | '--autoplay-policy=no-user-gesture-required',
23 | '--enable-usermedia-screen-capturing',
24 | '--allow-http-screen-capture',
25 | '--disable-gpu',
26 | '--start-fullscreen',
27 | '--window-size=' + win_size,
28 | '--force-device-scale-factor=' + `${scale_factor}`
29 | ],
30 | ignoreDefaultArgs: ['--mute-audio', '--enable-automation']
31 | });
32 | console.log("try new page .....");
33 | const page = await browser.newPage();
34 |
35 | await page.setViewport({
36 | width: Math.floor(w / scale_factor),
37 | height: Math.floor(h / scale_factor),
38 | deviceScaleFactor: scale_factor
39 | });
40 | console.log("try goto .....");
41 | url = args[1] || "http://dy-vedio.oss-cn-hangzhou.aliyuncs.com/video/a.mp4";
42 | //await page.goto(url, { waitUntil: 'networkidle0' });
43 | await goto(page, url)
44 | var timeout = parseInt(args[0], 10) * 1000;
45 | console.log("waitFor begin .....");
46 | // const session = await page.target().createCDPSession();
47 | // await session.send('Emulation.setPageScaleFactor', {
48 | // pageScaleFactor: 0.75, // 75%
49 | // });
50 | await page.waitForTimeout(timeout);
51 | // console.log("screenshot .....");
52 | // await page.screenshot({ path: '/var/output/test.png' });
53 | await browser.close();
54 | console.log("browser closed ...........");
55 | }
56 |
57 | async function sleep(seconds) {
58 | console.log(`sleeping ${seconds} seconds`);
59 | await new Promise(r => setTimeout(r, seconds * 1000));
60 | }
61 |
62 | function isRecoverableNetworkErrorMessage(message) {
63 | const re = /net::(ERR_NETWORK_CHANGED|ERR_CONNECTION_CLOSED)/;
64 | return re.test(message);
65 | }
66 |
67 | async function goto(page, url) {
68 | let interval = 1;
69 | const rate = 2;
70 | const count = 3
71 |
72 | for (let i = 0; i < count; i++) {
73 | try {
74 | console.log(`attempting ${i+1} to open ${url}...`);
75 | await page.goto(url, { waitUntil: 'networkidle0' }).then(()=>{
76 | const w_data= Buffer.from('success\n');
77 | fs.writeFile('load.log', w_data, {flag: 'w+'}, function (err) {
78 | if(err) {
79 | console.error(err);
80 | } else {
81 | console.log('写入成功');
82 | }
83 | });
84 | });
85 |
86 | console.log(`opened ${url}`);
87 | return;
88 | } catch (obj) {
89 | if (i < count - 1 && obj instanceof Error){
90 | if (isRecoverableNetworkErrorMessage(obj.message)) {
91 | await sleep(interval);
92 | interval = rate * interval;
93 | continue;
94 | }
95 | }
96 | throw obj;
97 | }
98 | }
99 | }
100 |
101 | record();
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/src/code/record.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | #set -v
4 |
5 | wait_ready(){
6 | echo "wait until $1 success ..."
7 | for i in {1..30}
8 | do
9 | count=`ps -ef | grep $1 | grep -v "grep" | wc -l`
10 | if [ $count -gt 0 ]; then
11 | echo "$1 is ready!"
12 | break
13 | else
14 | sleep 1
15 | fi
16 | done
17 | }
18 |
19 | wait_and_ensure_page_ready(){
20 | echo "wait and ensure page is ready...."
21 | for i in {1..30}
22 | do
23 | count=`find load.log | wc -l`
24 | if [ $count -gt 0 ]; then
25 | echo "page is ready!"
26 | break
27 | else
28 | sleep 1
29 | fi
30 | done
31 | }
32 |
33 | kill_pid () {
34 | local pids=`ps aux | grep $1 | grep -v grep | awk '{print $2}'`
35 | if [ "$pids" != "" ]; then
36 | echo "Killing the following $1 processes: $pids"
37 | kill -n $2 $pids
38 | else
39 | echo "No $1 processes to kill"
40 | fi
41 | }
42 |
43 | wait_shutdown(){
44 | echo "wait until $1 shutdown ..."
45 | for i in {1..30}
46 | do
47 | count=`ps -ef | grep $1 | grep -v "grep" | wc -l`
48 | if [ $count -eq 0 ]; then
49 | echo "$1 is shutdown!"
50 | break
51 | else
52 | sleep 1
53 | fi
54 | done
55 | }
56 |
57 | # start xvfb screen
58 | record_time=$1
59 | buff=300
60 | (( sleep_time = record_time + 5 ))
61 | (( node_time_out=record_time+buff ))
62 | echo "start xvfb-run ..."
63 | xvfb-run --listen-tcp --server-num=76 --server-arg="-screen 0 $3" --auth-file=$XAUTHORITY nohup node record.js $node_time_out $2 $4 $6 > /tmp/chrome.log 2>&1 &
64 |
65 | # start pulseaudio service
66 | pulseaudio -D --exit-idle-time=-1
67 | pacmd load-module module-virtual-sink sink_name=v1
68 | pacmd set-default-sink v1
69 | pacmd set-default-source v1.monitor
70 |
71 | wait_ready pulseaudio
72 | wait_ready xvfb-run
73 | wait_ready Xvfb
74 | wait_ready chrome
75 | wait_ready record.js
76 | wait_and_ensure_page_ready
77 | echo "no sleep----------------"
78 | #sleep 3s
79 |
80 | echo "ffmpeg start recording ..."
81 |
82 | if [ ! -n "$9" ] ;then
83 | nohup ffmpeg -y -loglevel debug -f x11grab -draw_mouse 0 -video_size $5 -r $7 -t $1 -i :76 -f alsa -ac 2 -ar 44100 -t $1 -i default -pix_fmt yuv420p /var/output/test.mp4 > /tmp/ffmpeg.log 2>&1 &
84 | else
85 | nohup ffmpeg -y -loglevel debug \
86 | -f x11grab -draw_mouse 0 -video_size $5 -framerate $7 -t $1 -i :76 \
87 | -f alsa -ac 2 -ar 44100 -t $1 -i default -pix_fmt yuv420p \
88 | -f mp4 -map 0 -map 1 /var/output/test.mp4 \
89 | -f flv -map 0 -map 1 $9 > /tmp/ffmpeg.log 2>&1 &
90 | fi
91 |
92 | wait_ready ffmpeg
93 |
94 | sleep $sleep_time
95 |
96 | # ffmpeg 必须先于 xvfb 退出
97 | echo "clean process ..."
98 | rm -rf load.log
99 | kill_pid ffmpeg 2
100 | wait_shutdown ffmpeg
101 |
102 | cat /tmp/ffmpeg.log
103 |
104 | ls -lh /var/output
105 |
106 | #sleep 3s
107 |
108 | kill_pid record.js 15
109 | wait_shutdown record.js
110 | kill_pid Xvfb 15
111 | wait_shutdown Xvfb
112 | kill_pid chrome 15
113 | wait_shutdown chrome
114 | kill_pid xvfb-run 15
115 | wait_shutdown xvfb-run
116 | kill_pid pulseaudio 15
117 | wait_shutdown pulseaudio
118 |
119 | #sleep 3s
120 |
121 | ps auxww
122 |
123 | echo "record worker finished!!!"
124 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/src/code/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Constants
4 | const PORT = 9000;
5 | const HOST = '0.0.0.0';
6 | const REQUEST_ID_HEADER = 'x-fc-request-id'
7 | const ACCESS_KEY_ID = 'x-fc-access-key-id'
8 | const ACCESS_KEY_SECRET = 'x-fc-access-key-secret'
9 | const SECURITY_TOKEN = 'x-fc-security-token'
10 |
11 | var execSync = require("child_process").execSync;
12 | const OSS = require('ali-oss');
13 | const express = require('express');
14 | const app = express();
15 | app.use(express.json())
16 |
17 | // invocation
18 | app.post('/', (req, res) => {
19 | // console.log(JSON.stringify(req.headers));
20 | var rid = req.headers[REQUEST_ID_HEADER]
21 | console.log(`FC Invoke Start RequestId: ${rid}`)
22 | try {
23 | // Prior to get recording parameters from request body to do your things
24 | console.log(JSON.stringify(req.body));
25 | var recordParams = req.body
26 | // Make video_url parameter as the inidcator
27 | if (!recordParams["video_url"]) {
28 | console.log("Miss mandotary video recording parameters in request body, try to get the parameters from query")
29 | // Fallback: Try to get recording parameters from query to do your things
30 | console.log(JSON.stringify(req.query));
31 | recordParams = req.query
32 | }
33 |
34 | // Compatible with old event mode
35 | var evt = recordParams
36 | if (!evt["video_url"]) {
37 | res.status(400).send("Miss mandotary video recording parameters");
38 | console.log(`FC Invoke End RequestId: ${rid}`)
39 | return
40 | }
41 | var recordTime = evt["record_time"];
42 | var videoUrl = evt["video_url"];
43 | var outputFile = evt["output_file"];
44 | var width = evt["width"];
45 | var height = evt["height"];
46 | var scale_factor = evt["scale"] || 1;
47 | var frame_rate = 30;
48 | if (evt["frame_rate"] != null) {
49 | frame_rate = evt["frame_rate"]
50 | }
51 | var bit_rate = "1500k"
52 | if (evt["bit_rate"] != null) {
53 | bit_rate = evt["bit_rate"]
54 | }
55 | var output_stream = ""
56 | if (evt["output_stream"] != null) {
57 | output_stream = evt["output_stream"];
58 | }
59 |
60 | var cmdStr = `/code/record.sh ${recordTime} '${videoUrl}' ${width}x${height}x24 ${width},${height} ${width}x${height} ${scale_factor} ${frame_rate} ${bit_rate} ${output_stream}`;
61 | console.log(`cmd is ${cmdStr} \n`);
62 | execSync(cmdStr, { stdio: 'inherit', shell: "/bin/bash" });
63 | console.log("start upload video to oss ...");
64 | const store = new OSS({
65 | accessKeyId: req.headers[ACCESS_KEY_ID],
66 | accessKeySecret: req.headers[ACCESS_KEY_SECRET],
67 | stsToken: req.headers[SECURITY_TOKEN],
68 | bucket: process.env.OSS_BUCKET,
69 | endpoint: process.env.OSS_ENDPOINT,
70 | });
71 | store.put(outputFile, '/var/output/test.mp4').then((result) => {
72 | console.log("finish to upload video to oss");
73 | execSync("rm -rf /var/output/test.mp4", { stdio: 'inherit' });
74 | res.send('OK');
75 | console.log(`FC Invoke End RequestId: ${rid}`)
76 | }).catch(function (e) {
77 | res.status(404).send(e.stack || e);
78 | console.log(`FC Invoke End RequestId: ${rid}, Error: Unhandled function error`);
79 | });
80 | } catch (e) {
81 | res.status(404).send(e.stack || e);
82 | console.log(`FC Invoke End RequestId: ${rid}, Error: Unhandled function error`)
83 | }
84 | });
85 |
86 | // TODO: write a common recording method
87 | function recording(recordParams, callback) {
88 |
89 | }
90 |
91 | // TODO: write a common uploading method
92 | function upload(uploadParams, callback) {
93 |
94 | }
95 |
96 | var server = app.listen(PORT, HOST);
97 | console.log(`Running on http://${HOST}:${PORT}`);
98 |
99 | server.timeout = 0; // never timeout
100 | server.keepAliveTimeout = 0; // keepalive, never timeout
101 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/src/dest/fail/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation fail: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/src/dest/succ/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation success: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/src/readme.md:
--------------------------------------------------------------------------------
1 | # HttpPanoramicPageRecording 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署一个Http触发的全景录制的应用到阿里云函数计算***
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## 部署 & 体验
32 |
33 |
34 |
35 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=HttpPanoramicPageRecording&type=direct) ,
36 | [](https://fcnext.console.aliyun.com/applications/create?template=HttpPanoramicPageRecording&type=direct) 该应用。
37 |
38 |
39 |
40 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
41 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
42 | - 初始化项目:`s init HttpPanoramicPageRecording -d HttpPanoramicPageRecording`
43 | - 进入项目,并进行项目部署:`cd HttpPanoramicPageRecording && s deploy -y`
44 |
45 |
46 |
47 |
48 |
49 | # 调用函数
50 |
51 | ``` bash
52 | # deploy
53 | $ s deploy -y --use-local
54 | # Invoke
55 | $ s invoke -e '{"record_time":"60","video_url":"https://tv.cctv.com/live/cctv1/","output_file":"record/test.mp4", "width":"1920", "height":"1080", "scale": 0.75, "frame_rate":25,"bit_rate":"2000k"}'
56 | ```
57 |
58 | 调用成功后, 会在对应的 bucket 下, 产生 record/test.mp4 大约 60 秒 1920x1080 的全景录制视频。
59 |
60 | 其中参数的意义:
61 |
62 | **1.record_time:** 录制时长
63 |
64 | **2.video_url:** 录制视频的 url
65 |
66 | **3.width:** 录制视频的宽度
67 |
68 | **4.height:** 录制视频的高度
69 |
70 | **5.scale:** 浏览器缩放比例
71 |
72 | **6.output_file:** 最后录制视频保存的 OSS 目录
73 |
74 | **7.frame_rate:** 录制视频的帧率(可不传递,默认帧率为30fps)
75 |
76 | **8.bit_rate:** 录制视频的码率(可不传递,默认码率为1500k)
77 |
78 | **9.output_stream:** 推流地址(可选参数,eg: rtmp://demo.aliyundoc.com/app/stream?xxxx)
79 |
80 | 其中 scale 是对浏览器进行 75% 的缩放,使视频能录制更多的网页内容
81 |
82 | **注意:** 如果您录制的视频存在一些卡顿或者快进, 可能是因为您录制的视频分辨率大并且复杂, 消耗的 CPU 很大, 您可以通过调大函数的规格, 提高 CPU 的能力。
83 |
84 | 比如上面的示例参数得到下图:
85 |
86 | 
87 |
88 | # 如何本地调试
89 |
90 | 直接本地运行, 命令执行完毕后, 会在当前目录生成一个 test.mp4 的视频
91 |
92 | ```bash
93 | # 直接本地执行docker命令, 会在当前目录生成一个 test.mp4 的视频
94 | $ docker run --rm --entrypoint="" -v $(pwd):/var/output aliyunfc/browser_recorder /code/record.sh 60 https://tv.cctv.com/live/cctv1 1920x1080x24 1920,1080 1920x1080 1 25 2000k
95 | ```
96 |
97 | 调试
98 |
99 | ```bash
100 | # 如果有镜像有代码更新, 重新build 镜像
101 | $ docker build -t my-panoramic-page-recording -f ./code/Dockerfile ./code
102 | # 测试全屏录制核心脚本 record.sh, 执行完毕后, 会在当前目录有一个 test.mp4 的视频
103 | $ docker run --rm --entrypoint="" -v $(pwd):/var/output my-panoramic-page-recording /code/record.sh 60 https://tv.cctv.com/live/cctv1 1920x1080x24 1920,1080 1920x1080 1 25 2000k
104 | ```
105 |
106 | > 其中 record.sh 的参数意义:
107 | >
108 | > 1. 录制时长
109 | > 2. 视频 url
110 | > 3. $widthx$heightx24
111 | > 4. $width,$height
112 | > 5. $widthx$height
113 | > 6. chrome 浏览器缩放比例
114 | > 7. 帧率
115 | > 8. 码率
116 | > 9. 推流地址
117 |
118 | # 原理
119 |
120 | Chrome 渲染到虚拟 X-server,并通过 FFmpeg 抓取系统桌⾯,通过启动 xvfb 启动虚拟 X-server,Chrome 进⾏全屏显示渲染到到虚拟 X-server 上,并通过 FFmpeg 抓取系统屏幕以及采集系统声⾳并进⾏编码写⽂件。这种⽅式的适配性⾮常好, 不仅可以录制 Chrome,理论上也可以录制其他的应⽤。缺点是占⽤的内存和 CPU 较多。
121 |
122 | **server.js**
123 |
124 | custom container http server 逻辑
125 |
126 | **record.sh**
127 |
128 | 核心录屏逻辑, 启动 xvfb, 在虚拟 X-server 中使用 `record.js` 中的 puppeteer 启动浏览器, 最后 FFmpeg 完成 X-server 屏幕的视频和音频抓取工作, 生成全屏录制后的视频
129 |
130 | # 其他
131 |
132 | 如果您想将生成的视频直接预热的 CDN, 以阿里云 CDN 为例, 只需要在 server.js 上传完 OSS bucket 后的逻辑中增加如下代码:
133 |
134 | [PushObjectCache](https://next.api.aliyun.com/api/Cdn/2018-05-10/PushObjectCache?lang=NODEJS&sdkStyle=old¶ms={})
135 |
136 | > Tips 前提需要配置好 CDN
137 |
138 |
139 |
140 |
141 |
142 | ## 开发者社区
143 |
144 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
145 |
146 |
147 |
148 | |
|
|
|
149 | |--- | --- | --- |
150 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/src/s.yaml:
--------------------------------------------------------------------------------
1 | # ------------------------------------
2 | # 欢迎您使用阿里云函数计算 FC 组件进行项目开发
3 | # 组件仓库地址:https://github.com/devsapp/fc
4 | # 组件帮助文档:https://www.serverless-devs.com/fc/readme
5 | # Yaml参考文档:https://www.serverless-devs.com/fc/yaml/readme
6 | # 关于:
7 | # - Serverless Devs和FC组件的关系、如何声明/部署多个函数、超过50M的代码包如何部署
8 | # - 关于.fcignore使用方法、工具中.s目录是做什么、函数进行build操作之后如何处理build的产物
9 | # 等问题,可以参考文档:https://www.serverless-devs.com/fc/tips
10 | # 关于如何做CICD等问题,可以参考:https://www.serverless-devs.com/serverless-devs/cicd
11 | # 关于如何进行环境划分等问题,可以参考:https://www.serverless-devs.com/serverless-devs/extend
12 | # 更多函数计算案例,可参考:https://github.com/devsapp/awesome/
13 | # 有问题快来钉钉群问一下吧:33947367
14 | # ------------------------------------
15 |
16 | edition: 1.0.0
17 | name: browser_video_recorder
18 | # access 是当前应用所需要的密钥信息配置:
19 | # 密钥配置可以参考:https://www.serverless-devs.com/serverless-devs/command/config
20 | # 密钥使用顺序可以参考:https://www.serverless-devs.com/serverless-devs/tool#密钥使用顺序与规范
21 | access: "{{ access }}"
22 |
23 | vars: # 全局变量
24 | region: "{{ region }}"
25 | service:
26 | name: "{{ serviceName }}"
27 | role: "{{ roleArn }}"
28 | description: 'Record a video for chrome browser'
29 | internetAccess: true
30 | functionName: "{{ functionName }}"
31 |
32 | services:
33 | browser_video_recorder_project: # 业务名称/模块名称
34 | component: fc # 组件名称,Serverless Devs 工具本身类似于一种游戏机,不具备具体的业务能力,组件类似于游戏卡,用户通过向游戏机中插入不同的游戏卡实现不同的功能,即通过使用不同的组件实现不同的具体业务能力
35 | actions:
36 | pre-deploy:
37 | - component: fc build --use-docker --dockerfile ./code/Dockerfile
38 | post-deploy:
39 | - component: fc api UpdateFunction --region ${vars.region} --header
40 | '{"x-fc-disable-container-reuse":"True"}' --path
41 | '{"serviceName":"${vars.service.name}","functionName":"${vars.functionName}"}'
42 | props:
43 | region: ${vars.region}
44 | service: ${vars.service}
45 | function:
46 | name: ${vars.functionName}
47 | runtime: custom-container
48 | memorySize: 8192
49 | instanceType: c1
50 | timeout: 7200
51 | customContainerConfig:
52 | image: "{{ acrRegistry }}"
53 | environmentVariables:
54 | OSS_BUCKET: "{{ ossBucket }}"
55 | OSS_ENDPOINT: oss-${vars.region}-internal.aliyuncs.com
56 | TZ: "{{ timeZone }}"
57 | asyncConfiguration:
58 | destination:
59 | # onSuccess: acs:fc:::services/${vars.service.name}/functions/dest-succ
60 | onFailure: acs:fc:::services/${vars.service.name}/functions/dest-fail
61 | maxAsyncEventAgeInSeconds: 18000
62 | maxAsyncRetryAttempts: 0
63 | statefulInvocation: true
64 | triggers:
65 | - name: httpTrigger
66 | type: http
67 | config:
68 | authType: anonymous
69 | methods:
70 | - POST
71 | dest-succ: # 业务名称/模块名称
72 | component: fc
73 | props: # 组件的属性值
74 | region: ${vars.region}
75 | service: ${vars.service}
76 | function:
77 | name: dest-succ
78 | description: 'async task destination success function by serverless devs'
79 | runtime: python3
80 | codeUri: ./dest/succ
81 | handler: index.handler
82 | memorySize: 512
83 | timeout: 60
84 |
85 | dest-fail: # 业务名称/模块名称
86 | component: fc
87 | props: # 组件的属性值
88 | region: ${vars.region}
89 | service: ${vars.service}
90 | function:
91 | name: dest-fail
92 | description: 'async task destination fail function by serverless devs'
93 | runtime: python3
94 | codeUri: ./dest/fail
95 | handler: index.handler
96 | memorySize: 512
97 | timeout: 60
98 |
--------------------------------------------------------------------------------
/serverless-panoramic-page-recording-http/version.md:
--------------------------------------------------------------------------------
1 | - 第一版
2 |
--------------------------------------------------------------------------------
/transcode/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | .s
3 |
--------------------------------------------------------------------------------
/transcode/hook/index.js:
--------------------------------------------------------------------------------
1 | async function preInit(inputObj) {
2 |
3 | }
4 |
5 | async function postInit(inputObj) {
6 | console.log(`\n _______ _______ __ __ _______ _______ _______
7 | | || || |_| || || || |
8 | | ___|| ___|| || _ || ___|| ___|
9 | | |___ | |___ | || |_| || |___ | | __
10 | | ___|| ___|| || ___|| ___|| || |
11 | | | | | | ||_|| || | | |___ | |_| |
12 | |___| |___| |_| |_||___| |_______||_______|
13 | `)
14 | console.log(`\n Welcome to the ffmpeg-app application
15 | This application requires to open these services:
16 | FC : https://fc.console.aliyun.com/
17 | This application can help you quickly deploy the ffmpeg-app project.
18 | The application uses FC component:https://github.com/devsapp/fc
19 | The application homepage: https://github.com/devsapp/start-ffmpeg\n`)
20 | }
21 |
22 | module.exports = {
23 | postInit,
24 | preInit
25 | }
26 |
--------------------------------------------------------------------------------
/transcode/publish.yaml:
--------------------------------------------------------------------------------
1 | Type: Application
2 | Name: video-transcode
3 | Provider:
4 | - 阿里云
5 | Version: 0.0.19
6 | Description: 快速部署音视频转码的应用到阿里云函数计算
7 | HomePage: https://github.com/devsapp/start-ffmpeg/tree/master/transcode
8 | Tags:
9 | - 部署函数
10 | - 音视频转码
11 | Category: 音视频处理
12 | Service:
13 | 函数计算:
14 | Authorities:
15 | - AliyunFCFullAccess
16 | Parameters:
17 | type: object
18 | additionalProperties: false # 不允许增加其他属性
19 | required: # 必填项
20 | - region
21 | - serviceName
22 | - roleArn
23 | - timeZone
24 | properties:
25 | region:
26 | title: 地域
27 | type: string
28 | default: cn-hangzhou
29 | description: 创建应用所在的地区
30 | enum:
31 | - cn-beijing
32 | - cn-hangzhou
33 | - cn-shanghai
34 | - cn-qingdao
35 | - cn-zhangjiakou
36 | - cn-huhehaote
37 | - cn-shenzhen
38 | - cn-chengdu
39 | - cn-hongkong
40 | - ap-southeast-1
41 | - ap-southeast-2
42 | - ap-southeast-3
43 | - ap-southeast-5
44 | - ap-northeast-1
45 | - eu-central-1
46 | - eu-west-1
47 | - us-west-1
48 | - us-east-1
49 | - ap-south-1
50 | serviceName:
51 | title: 服务名
52 | type: string
53 | default: VideoTranscoder-${default-suffix}
54 | pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$"
55 | description: 应用所属的函数计算服务
56 | required: true
57 | roleArn:
58 | title: RAM角色ARN
59 | type: string
60 | default: ""
61 | pattern: "^acs:ram::[0-9]*:role/.*$"
62 | description: "函数计算访问其他云服务时使用的服务角色,需要填写具体的角色ARN,格式为acs:ram::$account-id>:role/$role-name。例如:acs:ram::14310000000:role/aliyunfcdefaultrole。
63 | \n如果您没有特殊要求,可以使用函数计算提供的默认的服务角色,即AliyunFCDefaultRole, 并增加 AliyunOSSFullAccess 权限。如果您首次使用函数计算,可以访问 https://fcnext.console.aliyun.com 进行授权。
64 | \n详细文档参考 https://help.aliyun.com/document_detail/181589.html#section-o93-dbr-z6o"
65 | required: true
66 | x-role:
67 | name: fcffmpegrole
68 | service: fc
69 | authorities:
70 | - AliyunOSSFullAccess
71 | - AliyunFCDefaultRolePolicy
72 | timeZone:
73 | title: 时区
74 | type: string
75 | default: Asia/Shanghai
76 | description: 创建的应用函数执行时候所在实例的时区, 详情参考 https://docs.oracle.com/middleware/12211/wcs/tag-ref/MISC/TimeZones.html
77 | required: true
78 |
--------------------------------------------------------------------------------
/transcode/readme.md:
--------------------------------------------------------------------------------
1 | # video-transcode 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署音视频转码的应用到阿里云函数计算***
18 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ## 部署 & 体验
34 |
35 |
36 |
37 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=video-transcode) ,
38 | [](https://fcnext.console.aliyun.com/applications/create?template=video-transcode) 该应用。
39 |
40 |
41 |
42 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
43 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
44 | - 初始化项目:`s init video-transcode -d video-transcode`
45 | - 进入项目,并进行项目部署:`cd video-transcode && s deploy -y`
46 |
47 |
48 |
49 |
50 |
51 | # 应用详情
52 |
53 | 1. 发起 5 次异步任务函数调用
54 |
55 | ```bash
56 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"mov"}' --invocation-type async --stateful-async-invocation-id my1-480P-mp4
57 | VideoTranscoder/transcode async invoke success.
58 | request id: bf7d7745-886b-42fc-af21-ba87d98e1b1c
59 |
60 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"mov"}' --invocation-type async --stateful-async-invocation-id my2-480P-mp4
61 | VideoTranscoder/transcode async invoke success.
62 | request id: edb06071-ca26-4580-b0af-3959344cf5c3
63 |
64 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"flv"}' --invocation-type async --stateful-async-invocation-id my3-480P-mp4
65 | VideoTranscoder/transcode async invoke success.
66 | request id: 41101e41-3c0a-497a-b63c-35d510aef6fb
67 |
68 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"avi"}' --invocation-type async --stateful-async-invocation-id my4-480P-mp4
69 | VideoTranscoder/transcode async invoke success.
70 | request id: ff48cc04-c61b-4cd3-ae1b-1aaaa1f6c2b2
71 |
72 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"m3u8"}' --invocation-type async --stateful-async-invocation-id my5-480P-mp4
73 | VideoTranscoder/transcode async invoke success.
74 | request id: d4b02745-420c-4c9e-bc05-75cbdd2d010f
75 |
76 | ```
77 |
78 | 2. 登录[FC 控制台](https://fcnext.console.aliyun.com/)
79 |
80 | 
81 |
82 | 可以清晰看出每一次转码任务的执行情况:
83 |
84 | - A 视频是什么时候开始转码的, 什么时候转码结束
85 | - B 视频转码任务不太符合预期, 我中途可以点击停止调用
86 | - 通过调用状态过滤和时间窗口过滤,我可以知道现在有多少个任务正在执行, 历史完成情况是怎么样的
87 | - 可以追溯每次转码任务执行日志和触发payload
88 | - 当您的转码函数有异常时候, 会触发 dest-fail 函数的执行,您在这个函数可以添加您自定义的逻辑, 比如报警
89 | - ...
90 |
91 | 转码完毕后, 您也可以登录 OSS 控制台到指定的输出目录查看转码后的视频。
92 |
93 | > 在本地使用该项目时,不仅可以部署,还可以进行更多的操作,例如查看日志,查看指标,进行多种模式的调试等,这些操作详情可以参考[函数计算组件命令文档](https://github.com/devsapp/fc#%E6%96%87%E6%A1%A3%E7%9B%B8%E5%85%B3) ;
94 |
95 | ## 应用详情
96 |
97 | 本项目是基于函数计算打造一个 **Serverless架构的弹性高可用音视频处理系统**, 并且拥有以下优势:
98 |
99 | ### 拥有函数计算和Serverless工作流两个产品的优势
100 |
101 | * 无需采购和管理服务器等基础设施,只需专注视频处理业务逻辑的开发,大幅缩短项目交付时间、减少人力成本。
102 |
103 | * 提供日志查询、性能监控、报警等功能,可以快速排查故障。
104 |
105 | * 以事件驱动的方式触发响应请求。
106 |
107 | * 免运维,毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力,性能优异。
108 |
109 | * 成本极具竞争力。
110 |
111 |
112 |
113 | ### 相较于通用的转码处理服务的优点
114 |
115 | * 超强自定义,对用户透明,基于FFmpeg或其他音视频处理工具命令快速开发相应的音视频处理逻辑。
116 |
117 | * 一键迁移原基于FFmpeg自建的音视频处理服务。
118 |
119 | * 弹性更强,可以保证有充足的计算资源为转码服务,例如每周五定期产生几百个4 GB以上的1080P大视频,但是需要几小时内全部处理。
120 |
121 | * 音频格式的转换或各种采样率自定义、音频降噪等功能。例如专业音频处理工具AACgain和MP3Gain。
122 |
123 | * 可以和Serverless工作流完成更加复杂、自定义的任务编排。例如视频转码完成后,记录转码详情到数据库,同时自动将热度很高的视频预热到CDN上,从而缓解源站压力。
124 |
125 | * 更多方式的事件驱动,例如可以选择OSS自动触发,也可以根据业务选择MNS消息触发。
126 |
127 | * 在大部分场景下具有很强的成本竞争力。
128 |
129 |
130 |
131 | ### 相比于其他自建服务的优点
132 |
133 | * 毫秒级弹性伸缩,弹性能力超强,支持大规模资源调用,可弹性支持几万核的计算力,例如1万节课半个小时内完成转码。
134 |
135 | * 只需要专注业务逻辑代码即可,原生自带事件驱动模式,简化开发编程模型,同时可以达到消息,即音视频任务,处理的优先级,可大大提高开发运维效率。
136 |
137 | * 函数计算采用3AZ部署,安全性高,计算资源也是多AZ获取,能保证每位使用者需要的算力峰值。
138 |
139 | * 开箱即用的监控系统,可以多维度监控函数的执行情况,根据监控快速定位问题,同时给您提供分析能力。
140 |
141 | * 在大部分场景下具有很强的成本竞争力,因为函数计算是真正的按量付费,计费粒度在百毫秒,可以理解为CPU的利用率为100%。
142 |
143 |
144 | 通过 Serverless Devs 开发者工具,您只需要几步,就可以体验 Serverless 架构,带来的降本提效的技术红利。
145 |
146 |
147 |
148 |
149 |
150 |
151 | ## 开发者社区
152 |
153 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
154 |
155 |
156 |
157 | |
|
|
|
158 | |--- | --- | --- |
159 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/transcode/src/code/fail/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation fail: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/transcode/src/code/succ/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 |
5 | def handler(event, context):
6 | logger = logging.getLogger()
7 | logger.info('destnation success: {}'.format(event))
8 | # do your things
9 | # ...
10 | return {}
11 |
--------------------------------------------------------------------------------
/transcode/src/code/transcode/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 | import oss2
4 | import os
5 | import json
6 | import subprocess
7 | import shutil
8 |
9 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
10 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
11 | LOGGER = logging.getLogger()
12 |
13 |
14 | def get_fileNameExt(filename):
15 | (_, tempfilename) = os.path.split(filename)
16 | (shortname, extension) = os.path.splitext(tempfilename)
17 | return shortname, extension
18 |
19 |
20 | def handler(event, context):
21 | LOGGER.info(event)
22 | evt = json.loads(event)
23 | oss_bucket_name = evt["bucket"]
24 | object_key = evt["object"]
25 | output_dir = evt["output_dir"]
26 | dst_format = evt['dst_format']
27 | shortname, _ = get_fileNameExt(object_key)
28 | creds = context.credentials
29 | auth = oss2.StsAuth(creds.accessKeyId,
30 | creds.accessKeySecret, creds.securityToken)
31 | oss_client = oss2.Bucket(auth, 'oss-%s-internal.aliyuncs.com' %
32 | context.region, oss_bucket_name)
33 |
34 | # simplifiedmeta = oss_client.get_object_meta(object_key)
35 | # size = float(simplifiedmeta.headers['Content-Length'])
36 | # M_size = round(size / 1024.0 / 1024.0, 2)
37 |
38 | exist = oss_client.object_exists(object_key)
39 | if not exist:
40 | raise Exception("object {} is not exist".format(object_key))
41 |
42 | input_path = oss_client.sign_url('GET', object_key, 6 * 3600)
43 | # m3u8 特殊处理
44 | rid = context.request_id
45 | if dst_format == "m3u8":
46 | return handle_m3u8(rid, oss_client, input_path, shortname, output_dir)
47 | else:
48 | return handle_common(rid, oss_client, input_path, shortname, output_dir, dst_format)
49 |
50 |
51 | def handle_m3u8(request_id, oss_client, input_path, shortname, output_dir):
52 | ts_dir = '/tmp/ts'
53 | if os.path.exists(ts_dir):
54 | shutil.rmtree(ts_dir)
55 | os.mkdir(ts_dir)
56 | transcoded_filepath = os.path.join('/tmp', shortname + '.ts')
57 | split_transcoded_filepath = os.path.join(
58 | ts_dir, shortname + '_%03d.ts')
59 | cmd1 = ['ffmpeg', '-y', '-i', input_path, '-c:v',
60 | 'libx264', transcoded_filepath]
61 |
62 | cmd2 = ['ffmpeg', '-y', '-i', transcoded_filepath, '-c', 'copy', '-map', '0', '-f', 'segment',
63 | '-segment_list', os.path.join(ts_dir, 'playlist.m3u8'), '-segment_time', '10', split_transcoded_filepath]
64 |
65 | try:
66 | subprocess.run(
67 | cmd1, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
68 |
69 | subprocess.run(
70 | cmd2, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
71 |
72 | for filename in os.listdir(ts_dir):
73 | filepath = os.path.join(ts_dir, filename)
74 | filekey = os.path.join(output_dir, shortname, filename)
75 | oss_client.put_object_from_file(filekey, filepath)
76 | os.remove(filepath)
77 | print("Uploaded {} to {}".format(filepath, filekey))
78 |
79 | except subprocess.CalledProcessError as exc:
80 | # if transcode fail,trigger invoke dest-fail function
81 | raise Exception(request_id +
82 | " transcode failure, detail: " + str(exc))
83 |
84 | finally:
85 | if os.path.exists(ts_dir):
86 | shutil.rmtree(ts_dir)
87 |
88 | # remove ts 文件
89 | if os.path.exists(transcoded_filepath):
90 | os.remove(transcoded_filepath)
91 |
92 | return {}
93 |
94 |
95 | def handle_common(request_id, oss_client, input_path, shortname, output_dir, dst_format):
96 | transcoded_filepath = os.path.join('/tmp', shortname + '.' + dst_format)
97 | if os.path.exists(transcoded_filepath):
98 | os.remove(transcoded_filepath)
99 | cmd = ["ffmpeg", "-y", "-i", input_path, transcoded_filepath]
100 | try:
101 | subprocess.run(
102 | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
103 |
104 | oss_client.put_object_from_file(
105 | os.path.join(output_dir, shortname + '.' + dst_format), transcoded_filepath)
106 | except subprocess.CalledProcessError as exc:
107 | # if transcode fail,trigger invoke dest-fail function
108 | raise Exception(request_id +
109 | " transcode failure, detail: " + str(exc))
110 | finally:
111 | if os.path.exists(transcoded_filepath):
112 | os.remove(transcoded_filepath)
113 |
114 | return {}
115 |
--------------------------------------------------------------------------------
/transcode/src/readme.md:
--------------------------------------------------------------------------------
1 | # video-transcode 帮助文档
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | > ***快速部署音视频转码的应用到阿里云函数计算***
18 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ## 部署 & 体验
34 |
35 |
36 |
37 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=video-transcode) ,
38 | [](https://fcnext.console.aliyun.com/applications/create?template=video-transcode) 该应用。
39 |
40 |
41 |
42 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
43 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://www.serverless-devs.com/fc/config) ;
44 | - 初始化项目:`s init video-transcode -d video-transcode`
45 | - 进入项目,并进行项目部署:`cd video-transcode && s deploy -y`
46 |
47 |
48 |
49 |
50 |
51 | # 应用详情
52 |
53 | 1. 发起 5 次异步任务函数调用
54 |
55 | ```bash
56 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"mov"}' --invocation-type async --stateful-async-invocation-id my1-480P-mp4
57 | VideoTranscoder/transcode async invoke success.
58 | request id: bf7d7745-886b-42fc-af21-ba87d98e1b1c
59 |
60 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"mov"}' --invocation-type async --stateful-async-invocation-id my2-480P-mp4
61 | VideoTranscoder/transcode async invoke success.
62 | request id: edb06071-ca26-4580-b0af-3959344cf5c3
63 |
64 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"flv"}' --invocation-type async --stateful-async-invocation-id my3-480P-mp4
65 | VideoTranscoder/transcode async invoke success.
66 | request id: 41101e41-3c0a-497a-b63c-35d510aef6fb
67 |
68 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"avi"}' --invocation-type async --stateful-async-invocation-id my4-480P-mp4
69 | VideoTranscoder/transcode async invoke success.
70 | request id: ff48cc04-c61b-4cd3-ae1b-1aaaa1f6c2b2
71 |
72 | $ s VideoTranscoder invoke -e '{"bucket":"my-bucket", "object":"480P.mp4", "output_dir":"a", "dst_format":"m3u8"}' --invocation-type async --stateful-async-invocation-id my5-480P-mp4
73 | VideoTranscoder/transcode async invoke success.
74 | request id: d4b02745-420c-4c9e-bc05-75cbdd2d010f
75 |
76 | ```
77 |
78 | 2. 登录[FC 控制台](https://fcnext.console.aliyun.com/)
79 |
80 | 
81 |
82 | 可以清晰看出每一次转码任务的执行情况:
83 |
84 | - A 视频是什么时候开始转码的, 什么时候转码结束
85 | - B 视频转码任务不太符合预期, 我中途可以点击停止调用
86 | - 通过调用状态过滤和时间窗口过滤,我可以知道现在有多少个任务正在执行, 历史完成情况是怎么样的
87 | - 可以追溯每次转码任务执行日志和触发payload
88 | - 当您的转码函数有异常时候, 会触发 dest-fail 函数的执行,您在这个函数可以添加您自定义的逻辑, 比如报警
89 | - ...
90 |
91 | 转码完毕后, 您也可以登录 OSS 控制台到指定的输出目录查看转码后的视频。
92 |
93 | > 在本地使用该项目时,不仅可以部署,还可以进行更多的操作,例如查看日志,查看指标,进行多种模式的调试等,这些操作详情可以参考[函数计算组件命令文档](https://github.com/devsapp/fc#%E6%96%87%E6%A1%A3%E7%9B%B8%E5%85%B3) ;
94 |
95 | ## 应用详情
96 |
97 | 本项目是基于函数计算打造一个 **Serverless架构的弹性高可用音视频处理系统**, 并且拥有以下优势:
98 |
99 | ### 拥有函数计算和Serverless工作流两个产品的优势
100 |
101 | * 无需采购和管理服务器等基础设施,只需专注视频处理业务逻辑的开发,大幅缩短项目交付时间、减少人力成本。
102 |
103 | * 提供日志查询、性能监控、报警等功能,可以快速排查故障。
104 |
105 | * 以事件驱动的方式触发响应请求。
106 |
107 | * 免运维,毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力,性能优异。
108 |
109 | * 成本极具竞争力。
110 |
111 |
112 |
113 | ### 相较于通用的转码处理服务的优点
114 |
115 | * 超强自定义,对用户透明,基于FFmpeg或其他音视频处理工具命令快速开发相应的音视频处理逻辑。
116 |
117 | * 一键迁移原基于FFmpeg自建的音视频处理服务。
118 |
119 | * 弹性更强,可以保证有充足的计算资源为转码服务,例如每周五定期产生几百个4 GB以上的1080P大视频,但是需要几小时内全部处理。
120 |
121 | * 音频格式的转换或各种采样率自定义、音频降噪等功能。例如专业音频处理工具AACgain和MP3Gain。
122 |
123 | * 可以和Serverless工作流完成更加复杂、自定义的任务编排。例如视频转码完成后,记录转码详情到数据库,同时自动将热度很高的视频预热到CDN上,从而缓解源站压力。
124 |
125 | * 更多方式的事件驱动,例如可以选择OSS自动触发,也可以根据业务选择MNS消息触发。
126 |
127 | * 在大部分场景下具有很强的成本竞争力。
128 |
129 |
130 |
131 | ### 相比于其他自建服务的优点
132 |
133 | * 毫秒级弹性伸缩,弹性能力超强,支持大规模资源调用,可弹性支持几万核的计算力,例如1万节课半个小时内完成转码。
134 |
135 | * 只需要专注业务逻辑代码即可,原生自带事件驱动模式,简化开发编程模型,同时可以达到消息,即音视频任务,处理的优先级,可大大提高开发运维效率。
136 |
137 | * 函数计算采用3AZ部署,安全性高,计算资源也是多AZ获取,能保证每位使用者需要的算力峰值。
138 |
139 | * 开箱即用的监控系统,可以多维度监控函数的执行情况,根据监控快速定位问题,同时给您提供分析能力。
140 |
141 | * 在大部分场景下具有很强的成本竞争力,因为函数计算是真正的按量付费,计费粒度在百毫秒,可以理解为CPU的利用率为100%。
142 |
143 |
144 | 通过 Serverless Devs 开发者工具,您只需要几步,就可以体验 Serverless 架构,带来的降本提效的技术红利。
145 |
146 |
147 |
148 |
149 |
150 |
151 | ## 开发者社区
152 |
153 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
154 |
155 |
156 |
157 | |
|
|
|
158 | |--- | --- | --- |
159 | |
微信公众号:\`serverless\` | 微信小助手:\`xiaojiangwh\` | 钉钉交流群:\`33947367\` |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/transcode/src/s.yaml:
--------------------------------------------------------------------------------
1 | # ------------------------------------
2 | # 欢迎您使用阿里云函数计算 FC 组件进行项目开发
3 | # 组件仓库地址:https://github.com/devsapp/fc
4 | # 组件帮助文档:https://www.serverless-devs.com/fc/readme
5 | # Yaml参考文档:https://www.serverless-devs.com/fc/yaml/readme
6 | # 关于:
7 | # - Serverless Devs和FC组件的关系、如何声明/部署多个函数、超过50M的代码包如何部署
8 | # - 关于.fcignore使用方法、工具中.s目录是做什么、函数进行build操作之后如何处理build的产物
9 | # 等问题,可以参考文档:https://www.serverless-devs.com/fc/tips
10 | # 关于如何做CICD等问题,可以参考:https://www.serverless-devs.com/serverless-devs/cicd
11 | # 关于如何进行环境划分等问题,可以参考:https://www.serverless-devs.com/serverless-devs/extend
12 | # 更多函数计算案例,可参考:https://github.com/devsapp/awesome/
13 | # 有问题快来钉钉群问一下吧:33947367
14 | # ------------------------------------
15 |
16 | edition: 1.0.0
17 | name: video-transcode
18 | # access 是当前应用所需要的密钥信息配置:
19 | # 密钥配置可以参考:https://www.serverless-devs.com/serverless-devs/command/config
20 | # 密钥使用顺序可以参考:https://www.serverless-devs.com/serverless-devs/tool#密钥使用顺序与规范
21 | access: "{{ access }}"
22 |
23 | vars:
24 | region: "{{ region }}"
25 | service:
26 | name: "{{ serviceName }}"
27 | description: use ffmpeg to transcode video in FC
28 | internetAccess: true
29 | role: "{{ roleArn }}"
30 | # logConfig: auto
31 |
32 | services:
33 | VideoTranscoder: # 业务名称/模块名称
34 | component: fc # 组件名称,Serverless Devs 工具本身类似于一种游戏机,不具备具体的业务能力,组件类似于游戏卡,用户通过向游戏机中插入不同的游戏卡实现不同的功能,即通过使用不同的组件实现不同的具体业务能力
35 | # actions: # 自定义执行逻辑,关于actions 的使用,可以参考:https://www.serverless-devs.com/serverless-devs/yaml#行为描述
36 | # pre-deploy: # 在deploy之前运行
37 | # - run: s version publish -a demo
38 | # path: ./src
39 | # - run: docker build xxx # 要执行的系统命令,类似于一种钩子的形式
40 | # path: ./src # 执行系统命令/钩子的路径
41 | # - plugin: myplugin # 与运行的插件 (可以通过s cli registry search --type Plugin 获取组件列表)
42 | # args: # 插件的参数信息
43 | # testKey: testValue
44 | props:
45 | region: ${vars.region}
46 | service: ${vars.service}
47 | function:
48 | name: transcode
49 | runtime: python3
50 | Handler: index.handler
51 | codeUri: ./code/transcode
52 | memorySize: 8192
53 | timeout: 7200
54 | instanceType: c1
55 | environmentVariables:
56 | TZ: "{{ timeZone }}"
57 | asyncConfiguration:
58 | destination:
59 | # onSuccess: acs:fc:::services/${vars.service.name}/functions/dest-succ
60 | onFailure: acs:fc:::services/${vars.service.name}/functions/dest-fail
61 | maxAsyncEventAgeInSeconds: 18000
62 | maxAsyncRetryAttempts: 2
63 | statefulInvocation: true
64 |
65 | dest-succ: # 业务名称/模块名称
66 | component: fc
67 | props: # 组件的属性值
68 | region: ${vars.region}
69 | service: ${vars.service}
70 | function:
71 | name: dest-succ
72 | description: 'async task destination success function by serverless devs'
73 | runtime: python3
74 | codeUri: ./code/succ
75 | handler: index.handler
76 | memorySize: 512
77 | timeout: 60
78 |
79 | dest-fail: # 业务名称/模块名称
80 | component: fc
81 | props: # 组件的属性值
82 | region: ${vars.region}
83 | service: ${vars.service}
84 | function:
85 | name: dest-fail
86 | description: 'async task destination fail function by serverless devs'
87 | runtime: python3
88 | codeUri: ./code/fail
89 | handler: index.handler
90 | memorySize: 512
91 | timeout: 60
92 |
93 | # next-function: # 第二个函数的案例,仅供参考
94 | # # 如果在当前项目下执行 s deploy,会同时部署模块:
95 | # # helloworld:服务hello-world-service,函数cpp-event-function
96 | # # next-function:服务hello-world-service,函数next-function-example
97 | # # 如果想单独部署当前服务与函数,可以执行 s + 模块名/业务名 + deploy,例如:s next-function deploy
98 | # # 如果想单独部署当前函数,可以执行 s + 模块名/业务名 + deploy function,例如:s next-function deploy function
99 | # # 更多命令可参考:https://www.serverless-devs.com/fc/readme#文档相关
100 | # component: fc
101 | # props:
102 | # region: ${vars.region}
103 | # service: ${vars.service} # 应用整体的服务配置
104 | # function: # 定义一个新的函数
105 | # name: next-function-example
106 | # description: 'hello world by serverless devs'
107 |
--------------------------------------------------------------------------------
/transcode/version.md:
--------------------------------------------------------------------------------
1 | - 第一版
2 |
--------------------------------------------------------------------------------
/update.list:
--------------------------------------------------------------------------------
1 | ./video-process-flow
--------------------------------------------------------------------------------
/video-process-flow/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 | .s
3 |
--------------------------------------------------------------------------------
/video-process-flow/hook/index.js:
--------------------------------------------------------------------------------
1 | async function preInit(inputObj) {
2 |
3 | }
4 |
5 | async function postInit(inputObj) {
6 | console.log(`\n _______ _______ __ __ _______ _______ _______
7 | | || || |_| || || || |
8 | | ___|| ___|| || _ || ___|| ___|
9 | | |___ | |___ | || |_| || |___ | | __
10 | | ___|| ___|| || ___|| ___|| || |
11 | | | | | | ||_|| || | | |___ | |_| |
12 | |___| |___| |_| |_||___| |_______||_______|
13 | `)
14 | console.log(`\n Welcome to the ffmpeg-app application
15 | This application requires to open these services:
16 | FC : https://fc.console.aliyun.com/
17 | This application can help you quickly deploy the ffmpeg-app project.
18 | The application uses FC component:https://github.com/devsapp/fc
19 | The application homepage: https://github.com/devsapp/start-ffmpeg\n`)
20 |
21 | const { artTemplate } = inputObj;
22 | artTemplate("code/flows/video-processing-fc.yml");
23 | }
24 |
25 | module.exports = {
26 | postInit,
27 | preInit
28 | }
29 |
--------------------------------------------------------------------------------
/video-process-flow/publish.yaml:
--------------------------------------------------------------------------------
1 | Type: Application
2 | Name: video-process-flow
3 | Version: 0.1.6
4 | Provider:
5 | - 阿里云
6 | Description: 基于 FC + Serverless Workflow + OSS + NAS + FFmpeg 实现的弹性高可用、并行处理的视频转码服务
7 | HomePage: https://github.com/devsapp/start-ffmpeg/tree/master/video-process-flow
8 | Tags:
9 | - flow
10 | - ffmpeg
11 | - 音视频
12 | - 转码
13 | - 工作流
14 | Category: 音视频处理
15 | Service:
16 | 函数计算:
17 | Authorities:
18 | - AliyunFCFullAccess
19 | 硬盘挂载:
20 | Authorities:
21 | - AliyunNASFullAccess
22 | VPC:
23 | Authorities:
24 | - AliyunVPCFullAccess
25 | OSS:
26 | Authorities:
27 | - AliyunOSSFullAccess
28 | 工作流:
29 | Authorities:
30 | - AliyunFnFFullAccess
31 | 其它:
32 | Authorities:
33 | - AliyunECSFullAccess
34 | Parameters:
35 | type: object
36 | additionalProperties: false # 不允许增加其他属性
37 | required: # 必填项
38 | - region
39 | - serviceRoleArn
40 | - ossBucket
41 | - fnfRoleArn
42 | - prefix
43 | - outputDir
44 | - triggerRoleArn
45 | - segInterval
46 | - dstFormats
47 | properties:
48 | region:
49 | title: 地域
50 | type: string
51 | default: cn-hangzhou
52 | description: 创建应用所在的地区
53 | enum:
54 | - cn-beijing
55 | - cn-hangzhou
56 | - cn-shanghai
57 | - cn-shenzhen
58 | - ap-southeast-1
59 | - us-west-1
60 | serviceName:
61 | title: 服务名
62 | type: string
63 | default: video-process-flow-${default-suffix}
64 | pattern: "^[a-zA-Z_][a-zA-Z0-9-_]{0,127}$"
65 | description: 应用所属的函数计算服务
66 | serviceRoleArn:
67 | title: 函数计算Service RAM角色ARN
68 | type: string
69 | default: ""
70 | pattern: "^acs:ram::[0-9]*:role/.*$"
71 | description: "函数计算访问其他云服务时使用的服务角色,需要填写具体的角色ARN,格式为acs:ram::$account-id>:role/$role-name。例如:acs:ram::14310000000:role/aliyunfcdefaultrole。
72 | \n如果您没有特殊要求,可以使用函数计算提供的默认的服务角色,即AliyunFCDefaultRole, 并增加 AliyunOSSFullAccess 和 AliyunFnFFullAccess 权限。如果您首次使用函数计算,可以访问 https://fcnext.console.aliyun.com 进行授权。
73 | \n详细文档参考 https://help.aliyun.com/document_detail/181589.html#section-o93-dbr-z6o"
74 | required: true
75 | x-role:
76 | name: fcffmpegrole
77 | service: fc
78 | authorities:
79 | - AliyunOSSFullAccess
80 | - AliyunFCDefaultRolePolicy
81 | - AliyunFnFFullAccess
82 | ossBucket:
83 | title: 对象存储存储桶名
84 | type: string
85 | default: ""
86 | description: 用于 vscode 编辑器 workspace 和 data 的存储, 和函数在同一个 region
87 | required: true
88 | x-bucket:
89 | dependency:
90 | - region
91 | prefix:
92 | title: 前缀
93 | type: string
94 | default: src
95 | description: 建议设置精准的前缀,同一个 Bucket 下的不同触发器条件不能重叠包含
96 |
97 | outputDir:
98 | title: 转码后的视频保存目录
99 | type: string
100 | default: dst
101 | description: 转码后的视频保存目录。为防止循环触发产生不必要的费用,强烈建议您设置不同于前缀的目标目录。
102 |
103 | triggerRoleArn:
104 | title: OSS触发器RAM角色ARN
105 | type: string
106 | default: ""
107 | pattern: "^acs:ram::[0-9]*:role/.*$"
108 | description: OSS使用此角色来发送事件通知来调用函数
109 | required: true
110 | x-role:
111 | name: aliyunosseventnotificationrole
112 | service: OSS
113 | authorities:
114 | - AliyunFCInvocationAccess
115 |
116 | segInterval:
117 | title: 对视频进行分片处理的分片时间
118 | type: string
119 | default: "30"
120 | description: 对视频进行分片处理的分片时间,单位为秒
121 |
122 | dstFormats:
123 | title: 转码后的视频格式
124 | type: string
125 | default: mp4, flv, avi
126 | description: 转码后的视频格式,如果有需要输出多种格式, 使用逗号分隔
127 |
128 | flowName:
129 | title: 工作流程名称
130 | type: string
131 | default: video-process-flow
132 | description: Serverless 工作流流程名称
133 |
134 | fnfRoleArn:
135 | title: 工作流 RAM角色ARN
136 | type: string
137 | default: ""
138 | pattern: "^acs:ram::[0-9]*:role/.*$"
139 | description: 应用所属的工作流需要的 role, 请提前创建好对应的 role, 授信工作流服务, 并配置好 AliyunFCInvocationAccess 和 AliyunFnFFullAccess policy。
140 | required: true
141 | x-role:
142 | name: fnf-execution-default-role
143 | service: FNF
144 | authorities:
145 | - AliyunFCInvocationAccess
146 | - AliyunFnFFullAccess
147 |
--------------------------------------------------------------------------------
/video-process-flow/readme.md:
--------------------------------------------------------------------------------
1 | src/readme.md
--------------------------------------------------------------------------------
/video-process-flow/src/code/after-process/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import json
4 | import shutil
5 |
6 | def handler(event, context):
7 | evt = json.loads(event)
8 | video_process_dir = evt['video_proc_dir']
9 | # delete all files in nas
10 | shutil.rmtree(video_process_dir)
11 |
12 | # do your logic, for example, insert/update a record in to db
13 |
14 | return {}
15 |
--------------------------------------------------------------------------------
/video-process-flow/src/code/flows/input-fc.json:
--------------------------------------------------------------------------------
1 | {
2 | "oss_bucket_name": "fnf-test",
3 | "video_key": "fnf_video/inputs/fc-official-short.mov",
4 | "output_prefix": "fnf_video/outputs/fc/1",
5 | "segment_time_seconds": 15,
6 | "dst_formats": ["mp4", "flv"]
7 | }
--------------------------------------------------------------------------------
/video-process-flow/src/code/flows/video-processing-fc.yml:
--------------------------------------------------------------------------------
1 | version: v1beta1
2 | type: flow
3 | steps:
4 | - type: task
5 | name: Split
6 | resourceArn: 'acs:fc:::services/{{serviceName}}/functions/split'
7 | retry:
8 | - errors:
9 | - FC.ResourceThrottled
10 | - FC.ResourceExhausted
11 | - FC.InternalServerError
12 | - FnF.TaskTimeout
13 | - FC.Unknown
14 | intervalSeconds: 3
15 | maxAttempts: 16
16 | multiplier: 2
17 | - type: foreach
18 | name: ParallelTranscode
19 | iterationMapping:
20 | collection: $.dst_formats
21 | index: index
22 | item: target_type
23 | steps:
24 | - type: foreach
25 | name: Transcode_splits
26 | iterationMapping:
27 | collection: $.split_keys
28 | index: index
29 | item: split_video_key
30 | steps:
31 | - type: task
32 | name: Transcode
33 | resourceArn: 'acs:fc:::services/{{serviceName}}/functions/transcode'
34 | retry:
35 | - errors:
36 | - FC.ResourceThrottled
37 | - FC.ResourceExhausted
38 | - FC.InternalServerError
39 | - FnF.TaskTimeout
40 | - FC.Unknown
41 | intervalSeconds: 3
42 | maxAttempts: 16
43 | multiplier: 2
44 | - type: task
45 | name: Merge
46 | resourceArn: 'acs:fc:::services/{{serviceName}}/functions/merge'
47 | retry:
48 | - errors:
49 | - FC.ResourceThrottled
50 | - FC.ResourceExhausted
51 | - FC.InternalServerError
52 | - FnF.TaskTimeout
53 | - FC.Unknown
54 | intervalSeconds: 3
55 | maxAttempts: 16
56 | multiplier: 2
57 | outputMappings:
58 | - target: video_proc_dir
59 | source: $input.video_proc_dir
60 | - type: task
61 | name: after-process
62 | resourceArn: 'acs:fc:::services/{{serviceName}}/functions/after-process'
63 | retry:
64 | - errors:
65 | - FC.ResourceThrottled
66 | - FC.ResourceExhausted
67 | - FC.InternalServerError
68 | - FnF.TaskTimeout
69 | - FC.Unknown
70 | intervalSeconds: 3
71 | maxAttempts: 16
72 | multiplier: 2
--------------------------------------------------------------------------------
/video-process-flow/src/code/merge/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import subprocess
3 | import oss2
4 | import logging
5 | import json
6 | import os
7 | import time
8 | import shutil
9 |
10 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
11 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
12 |
13 | LOGGER = logging.getLogger()
14 |
15 |
16 | class FFmpegError(Exception):
17 | def __init__(self, message, status):
18 | super().__init__(message, status)
19 | self.message = message
20 | self.status = status
21 |
22 | def exec_FFmpeg_cmd(cmd_lst):
23 | try:
24 | subprocess.run(
25 | cmd_lst, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
26 | except subprocess.CalledProcessError as exc:
27 | LOGGER.error('returncode:{}'.format(exc.returncode))
28 | LOGGER.error('cmd:{}'.format(exc.cmd))
29 | LOGGER.error('output:{}'.format(exc.output))
30 | LOGGER.error('stderr:{}'.format(exc.stderr))
31 | LOGGER.error('stdout:{}'.format(exc.stdout))
32 | # log json to Log Service as db
33 | # or insert record in mysql, etc ...
34 | raise FFmpegError(exc.output, exc.returncode)
35 |
36 | # a decorator for print the excute time of a function
37 | def print_excute_time(func):
38 | def wrapper(*args, **kwargs):
39 | local_time = time.time()
40 | ret = func(*args, **kwargs)
41 | LOGGER.info('current Function [%s] excute time is %.2f' %
42 | (func.__name__, time.time() - local_time))
43 | return ret
44 | return wrapper
45 |
46 |
47 | def get_fileNameExt(filename):
48 | (fileDir, tempfilename) = os.path.split(filename)
49 | (shortname, extension) = os.path.splitext(tempfilename)
50 | return fileDir, shortname, extension
51 |
52 |
53 | @print_excute_time
54 | def handler(event, context):
55 | evt = json.loads(event)
56 | video_key = evt['video_key']
57 | oss_bucket_name = evt['oss_bucket_name']
58 | split_keys = evt['split_keys']
59 | output_prefix = evt['output_prefix']
60 | video_type = evt['target_type']
61 | video_process_dir = evt['video_proc_dir']
62 |
63 | transcoded_split_keys = []
64 | for k in split_keys:
65 | fileDir, shortname, extension = get_fileNameExt(k)
66 | transcoded_filename = 'transcoded_%s.%s' % (shortname, video_type)
67 | transcoded_filepath = os.path.join(fileDir, transcoded_filename)
68 | transcoded_split_keys.append(transcoded_filepath)
69 |
70 | creds = context.credentials
71 | auth = oss2.StsAuth(creds.accessKeyId,
72 | creds.accessKeySecret, creds.securityToken)
73 | oss_client = oss2.Bucket(
74 | auth, 'oss-%s-internal.aliyuncs.com' % context.region, oss_bucket_name)
75 |
76 | if len(transcoded_split_keys) == 0:
77 | raise Exception("no transcoded_split_keys")
78 |
79 | LOGGER.info({
80 | "target_type": video_type,
81 | "transcoded_split_keys": transcoded_split_keys
82 | })
83 |
84 | _, shortname, extension = get_fileNameExt(video_key)
85 | segs_filename = 'segs_%s.txt' % (shortname + video_type)
86 | segs_filepath = os.path.join(video_process_dir, segs_filename)
87 |
88 | if os.path.exists(segs_filepath):
89 | os.remove(segs_filepath)
90 |
91 | with open(segs_filepath, 'a+') as f:
92 | for filepath in transcoded_split_keys:
93 | f.write("file '%s'\n" % filepath)
94 |
95 | merged_filename = 'merged_' + shortname + "." + video_type
96 | merged_filepath = os.path.join(video_process_dir, merged_filename)
97 |
98 | if os.path.exists(merged_filepath):
99 | os.remove(merged_filepath)
100 |
101 | exec_FFmpeg_cmd(['ffmpeg', '-f', 'concat', '-safe', '0', '-i',
102 | segs_filepath, '-c', 'copy', '-fflags', '+genpts', merged_filepath])
103 |
104 | LOGGER.info('output_prefix ' + output_prefix)
105 | merged_key = os.path.join(output_prefix, shortname, merged_filename)
106 | oss_client.put_object_from_file(merged_key, merged_filepath)
107 | LOGGER.info("Uploaded %s to %s" % (merged_filepath, merged_key))
108 |
109 | res = {
110 | video_type: merged_key,
111 | "video_proc_dir": video_process_dir
112 | }
113 |
114 | return res
115 |
--------------------------------------------------------------------------------
/video-process-flow/src/code/oss-trigger/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import os
3 | import json
4 | import re
5 | import logging
6 |
7 | from aliyunsdkcore.client import AcsClient
8 | from aliyunsdkfnf.request.v20190315 import StartExecutionRequest
9 | from aliyunsdkcore.acs_exception.exceptions import ServerException
10 | from aliyunsdkcore.auth.credentials import StsTokenCredential
11 |
12 | LOGGER = logging.getLogger()
13 |
14 | OUTPUT_DST = os.environ["OUTPUT_DST"]
15 | FLOW_NAME = os.environ["FLOW_NAME"]
16 | SEG_INTERVAL = os.environ["SEG_INTERVAL"]
17 | DST_FORMATS = os.environ["DST_FORMATS"]
18 |
19 | def handler(event, context):
20 | evt = json.loads(event)
21 | evt = evt["events"]
22 | oss_bucket_name = evt[0]["oss"]["bucket"]["name"]
23 | object_key = evt[0]["oss"]["object"]["key"]
24 |
25 | creds = context.credentials
26 | sts_token_credential = StsTokenCredential(creds.access_key_id, creds.access_key_secret, creds.security_token)
27 | client = AcsClient(region_id=context.region, credential=sts_token_credential)
28 |
29 | dst_formats = DST_FORMATS.split(",")
30 | dst_formats = [i.strip() for i in dst_formats]
31 |
32 | input = {
33 | "oss_bucket_name": oss_bucket_name,
34 | "video_key": object_key,
35 | "output_prefix": OUTPUT_DST,
36 | "segment_time_seconds": int(SEG_INTERVAL),
37 | "dst_formats": dst_formats
38 | }
39 |
40 | try:
41 | request = StartExecutionRequest.StartExecutionRequest()
42 | request.set_FlowName(FLOW_NAME)
43 | request.set_Input(json.dumps(input))
44 | execution_name = re.sub(
45 | r"[^a-zA-Z0-9-_]", "_", object_key) + "-" + context.request_id
46 | request.set_ExecutionName(execution_name)
47 | return client.do_action_with_exception(request)
48 | except ServerException as e:
49 | LOGGER.info(e.get_request_id())
50 |
51 | return "ok"
52 |
--------------------------------------------------------------------------------
/video-process-flow/src/code/split/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import subprocess
3 | import oss2
4 | import logging
5 | import json
6 | import os
7 | import time
8 | import math
9 |
10 | logging.getLogger("oss2.api").setLevel(logging.ERROR)
11 | logging.getLogger("oss2.auth").setLevel(logging.ERROR)
12 |
13 | LOGGER = logging.getLogger()
14 |
15 | MAX_SPLIT_NUM = 100
16 |
17 | NAS_ROOT = "/mnt/auto/"
18 |
19 | class FFmpegError(Exception):
20 | def __init__(self, message, status):
21 | super().__init__(message, status)
22 | self.message = message
23 | self.status = status
24 |
25 | def exec_FFmpeg_cmd(cmd_lst):
26 | try:
27 | subprocess.run(
28 | cmd_lst, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
29 | except subprocess.CalledProcessError as exc:
30 | LOGGER.error('returncode:{}'.format(exc.returncode))
31 | LOGGER.error('cmd:{}'.format(exc.cmd))
32 | LOGGER.error('output:{}'.format(exc.output))
33 | LOGGER.error('stderr:{}'.format(exc.stderr))
34 | LOGGER.error('stdout:{}'.format(exc.stdout))
35 | # log json to Log Service as db
36 | # or insert record in mysql, etc ...
37 | raise FFmpegError(exc.output, exc.returncode)
38 |
39 | # a decorator for print the excute time of a function
40 | def print_excute_time(func):
41 | def wrapper(*args, **kwargs):
42 | local_time = time.time()
43 | ret = func(*args, **kwargs)
44 | LOGGER.info('current Function [%s] excute time is %.2f' %
45 | (func.__name__, time.time() - local_time))
46 | return ret
47 | return wrapper
48 |
49 | def get_fileNameExt(filename):
50 | (fileDir, tempfilename) = os.path.split(filename)
51 | (shortname, extension) = os.path.splitext(tempfilename)
52 | return shortname, extension
53 |
54 | def getVideoDuration(input_video):
55 | cmd = '{0} -i {1} -show_entries format=duration -v quiet -of csv="p=0"'.format(
56 | 'ffprobe', input_video)
57 | raw_result = subprocess.check_output(cmd, shell=True)
58 | result = raw_result.decode().replace("\n", "").strip()
59 | duration = float(result)
60 | return duration
61 |
62 | @print_excute_time
63 | def handler(event, context):
64 | evt = json.loads(event)
65 | video_key = evt['video_key']
66 | oss_bucket_name = evt['oss_bucket_name']
67 | segment_time_seconds = str(evt['segment_time_seconds'])
68 |
69 | shortname, extension = get_fileNameExt(video_key)
70 | video_name = shortname + extension
71 |
72 | video_proc_dir = NAS_ROOT + context.request_id
73 | os.mkdir(video_proc_dir)
74 | os.system("chmod -R 777 " + video_proc_dir)
75 |
76 | creds = context.credentials
77 | auth = oss2.StsAuth(creds.accessKeyId, creds.accessKeySecret, creds.securityToken)
78 | oss_client = oss2.Bucket(auth, 'oss-%s-internal.aliyuncs.com' % context.region, oss_bucket_name)
79 |
80 | input_path = os.path.join(video_proc_dir, video_name)
81 | obj = oss_client.get_object_to_file(video_key, input_path)
82 |
83 | video_duration = getVideoDuration(input_path)
84 | segment_time_seconds = int(segment_time_seconds)
85 | split_num = math.ceil(video_duration/segment_time_seconds)
86 | # adjust segment_time_seconds
87 | if split_num > MAX_SPLIT_NUM:
88 | segment_time_seconds = int(math.ceil(video_duration/MAX_SPLIT_NUM)) + 1
89 |
90 | segment_time_seconds = str(segment_time_seconds)
91 | exec_FFmpeg_cmd(['ffmpeg', '-i', input_path, "-c", "copy", "-f", "segment", "-segment_time",
92 | segment_time_seconds, "-reset_timestamps", "1", video_proc_dir + "/split_" + shortname + '_piece_%02d' + extension])
93 |
94 | split_keys = []
95 | for filename in os.listdir(video_proc_dir):
96 | if filename.startswith('split_' + shortname):
97 | filekey = os.path.join(video_proc_dir, filename)
98 | split_keys.append(filekey)
99 |
100 | return {
101 | "split_keys": split_keys,
102 | "video_proc_dir": video_proc_dir
103 | }
104 |
--------------------------------------------------------------------------------
/video-process-flow/src/code/transcode/index.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import subprocess
3 | import logging
4 | import json
5 | import os
6 | import time
7 |
8 | LOGGER = logging.getLogger()
9 |
10 |
11 | class FFmpegError(Exception):
12 | def __init__(self, message, status):
13 | super().__init__(message, status)
14 | self.message = message
15 | self.status = status
16 |
17 | def exec_FFmpeg_cmd(cmd_lst):
18 | try:
19 | subprocess.run(
20 | cmd_lst, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
21 | except subprocess.CalledProcessError as exc:
22 | LOGGER.error('returncode:{}'.format(exc.returncode))
23 | LOGGER.error('cmd:{}'.format(exc.cmd))
24 | LOGGER.error('output:{}'.format(exc.output))
25 | LOGGER.error('stderr:{}'.format(exc.stderr))
26 | LOGGER.error('stdout:{}'.format(exc.stdout))
27 | # log json to Log Service as db
28 | # or insert record in mysql, etc ...
29 | raise FFmpegError(exc.output, exc.returncode)
30 |
31 | # a decorator for print the excute time of a function
32 | def print_excute_time(func):
33 | def wrapper(*args, **kwargs):
34 | local_time = time.time()
35 | ret = func(*args, **kwargs)
36 | LOGGER.info('current Function [%s] excute time is %.2f' %
37 | (func.__name__, time.time() - local_time))
38 | return ret
39 | return wrapper
40 |
41 | def get_fileNameExt(filename):
42 | (fileDir, tempfilename) = os.path.split(filename)
43 | (shortname, extension) = os.path.splitext(tempfilename)
44 | return fileDir, shortname, extension
45 |
46 | @print_excute_time
47 | def handler(event, context):
48 | evt = json.loads(event)
49 | # split video key, locate in nas
50 | input_path = evt['split_video_key']
51 | fileDir, shortname, extension = get_fileNameExt(input_path)
52 |
53 | target_type = evt['target_type']
54 | transcoded_filename = 'transcoded_%s.%s' % (shortname, target_type)
55 | transcoded_filepath = os.path.join(fileDir, transcoded_filename)
56 |
57 | if os.path.exists(transcoded_filepath):
58 | os.remove(transcoded_filepath)
59 |
60 | exec_FFmpeg_cmd(['ffmpeg', '-y', '-i', input_path, transcoded_filepath])
61 | return {}
62 |
63 |
--------------------------------------------------------------------------------
/video-process-flow/src/readme.md:
--------------------------------------------------------------------------------
1 |
2 | > 注:当前项目为 Serverless Devs 应用,由于应用中会存在需要初始化才可运行的变量(例如应用部署地区、服务名、函数名等等),所以**不推荐**直接 Clone 本仓库到本地进行部署或直接复制 s.yaml 使用,**强烈推荐**通过 `s init ` 的方法或应用中心进行初始化,详情可参考[部署 & 体验](#部署--体验) 。
3 |
4 | # video-process-flow 帮助文档
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 基于 FC + Serverless Workflow + OSS + NAS + FFmpeg 实现的弹性高可用、并行处理的视频转码服务
20 |
21 |
22 |
23 |
24 |
25 | - [:smiley_cat: 代码](https://github.com/devsapp/start-ffmpeg/tree/master/video-process-flow/src)
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ## 前期准备
36 |
37 | 使用该项目,您需要有开通以下服务:
38 |
39 |
40 |
41 |
42 |
43 | | 服务 | 备注 |
44 | | --- | --- |
45 | | 函数计算 FC | 转码等函数部署在函数计算 |
46 | | Serverless 工作流 | 视频处理工作流部署在 Serverless 工作流 |
47 | | 对象存储 OSS | 原视频位于 OSS |
48 | | 文件存储 NAS | 视频临时处理工作区间位于文件存储 NAS |
49 | | 专有网络 VPC | NAS 挂载点需要有 VPC |
50 |
51 |
52 |
53 | 推荐您拥有以下的产品权限 / 策略:
54 |
55 |
56 |
57 |
58 | | 服务/业务 | 权限 | 备注 |
59 | | --- | --- | --- |
60 | | 函数计算 | AliyunFCFullAccess | 创建或者更新转码等函数 |
61 | | 硬盘挂载 | AliyunNASFullAccess | 视频临时处理工作区间位于文件存储 NAS, 需要有自动创建 NAS 的权限 |
62 | | VPC | AliyunVPCFullAccess | NAS 需要 VPC 挂载点, 需要有 VPC 自动创建的能力 |
63 | | OSS | AliyunOSSFullAccess | 创建 OSS 触发器需要的调用 OSS 相关 API 的权限 |
64 | | 工作流 | AliyunFnFFullAccess | 创建或者更新音视频处理工作流 |
65 | | 其它 | AliyunECSFullAccess | 函数计算 NAS 挂载点需要交换机和安全组, 需要有自动创建的权限 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | ## 部署 & 体验
82 |
83 |
84 |
85 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=video-process-flow) ,
86 | [](https://fcnext.console.aliyun.com/applications/create?template=video-process-flow) 该应用。
87 |
88 |
89 |
90 |
91 | - 通过 [Serverless Devs Cli](https://www.serverless-devs.com/serverless-devs/install) 进行部署:
92 | - [安装 Serverless Devs Cli 开发者工具](https://www.serverless-devs.com/serverless-devs/install) ,并进行[授权信息配置](https://docs.serverless-devs.com/fc/config) ;
93 | - 初始化项目:`s init video-process-flow -d video-process-flow `
94 | - 进入项目,并进行项目部署:`cd video-process-flow && s deploy - y`
95 |
96 |
97 |
98 | ## 应用详情
99 |
100 |
101 |
102 | 如下图所示, 假设用户上传一个 mov 格式的视频到 OSS, OSS 触发器自动触发函数执行, 函数调用 FnF 执行,FnF 同时进行 1 种或者多种格式的转码(由 s.yaml 中的 DST_FORMATS 参数控制), 本示例配置的是同时进行 mp4, flv, avi 格式的转码。
103 |
104 | 您可以实现如下需求:
105 |
106 | - 一个视频文件可以同时被转码成各种格式以及其他各种自定义处理,比如增加水印处理或者在 after-process 更新信息到数据库等。
107 |
108 | - 当有多个文件同时上传到 OSS,函数计算会自动伸缩, 并行处理多个文件, 同时每次文件转码成多种格式也是并行。
109 |
110 | - 结合 NAS + 视频切片, 可以解决超大视频的转码, 对于每一个视频,先进行切片处理,然后并行转码切片,最后合成,通过设置合理的切片时间,可以大大加速较大视频的转码速度。
111 |
112 | 
113 |
114 |
115 |
116 | ## 使用文档
117 |
118 |
119 |
120 | **操作视频教程:**
121 |
122 | [](http://devsapp.functioncompute.com/video/video-process-flow.mp4)
123 |
124 | **P.S.** 当您想要仅在一个简单的函数中直接完成视频处理逻辑时,可以参考[音视频转码 Job](https://github.com/devsapp/start-ffmpeg/tree/master/transcode)
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | ## 开发者社区
133 |
134 | 您如果有关于错误的反馈或者未来的期待,您可以在 [Serverless Devs repo Issues](https://github.com/serverless-devs/serverless-devs/issues) 中进行反馈和交流。如果您想要加入我们的讨论组或者了解 FC 组件的最新动态,您可以通过以下渠道进行:
135 |
136 |
137 |
138 | |
|
|
|
139 | | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
140 | |
微信公众号:`serverless` | 微信小助手:`xiaojiangwh` | 钉钉交流群:`33947367` |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/video-process-flow/src/s.yaml:
--------------------------------------------------------------------------------
1 | # ------------------------------------
2 | # 欢迎您使用阿里云函数计算 FC 组件进行项目开发
3 | # 组件仓库地址:https://github.com/devsapp/fc
4 | # 组件帮助文档:https://www.serverless-devs.com/fc/readme
5 | # Yaml参考文档:https://www.serverless-devs.com/fc/yaml/readme
6 | # 关于:
7 | # - Serverless Devs和FC组件的关系、如何声明/部署多个函数、超过50M的代码包如何部署
8 | # - 关于.fcignore使用方法、工具中.s目录是做什么、函数进行build操作之后如何处理build的产物
9 | # 等问题,可以参考文档:https://www.serverless-devs.com/fc/tips
10 | # 关于如何做CICD等问题,可以参考:https://www.serverless-devs.com/serverless-devs/cicd
11 | # 关于如何进行环境划分等问题,可以参考:https://www.serverless-devs.com/serverless-devs/extend
12 | # 更多函数计算案例,可参考:https://github.com/devsapp/awesome/
13 | # 有问题快来钉钉群问一下吧:33947367
14 | # ------------------------------------
15 |
16 | edition: 1.0.0
17 | name: video-process-flow
18 | # access 是当前应用所需要的密钥信息配置:
19 | # 密钥配置可以参考:https://www.serverless-devs.com/serverless-devs/command/config
20 | # 密钥使用顺序可以参考:https://www.serverless-devs.com/serverless-devs/tool#密钥使用顺序与规范
21 | access: "{{ access }}"
22 |
23 | vars:
24 | region: "{{ region }}"
25 | service:
26 | name: "{{ serviceName }}"
27 | description: use fc+fnf+ffmpeg to transcode video in FC
28 | internetAccess: true
29 | role: "{{ serviceRoleArn }}"
30 | nasConfig: auto
31 | # logConfig: auto
32 | flowName: "{{ flowName }}"
33 |
34 | services:
35 | # 函数计算配置
36 | fc-video-demo-split:
37 | component: devsapp/fc
38 | props:
39 | region: ${vars.region}
40 | service: ${vars.service}
41 | function:
42 | name: split
43 | handler: index.handler
44 | timeout: 600
45 | memorySize: 3072
46 | runtime: python3
47 | codeUri: code/split
48 | fc-video-demo-transcode:
49 | component: devsapp/fc
50 | props:
51 | region: ${vars.region}
52 | service: ${vars.service}
53 | function:
54 | name: transcode
55 | handler: index.handler
56 | timeout: 600
57 | memorySize: 3072
58 | runtime: python3
59 | codeUri: code/transcode
60 | fc-video-demo-merge:
61 | component: devsapp/fc
62 | props:
63 | region: ${vars.region}
64 | service: ${vars.service}
65 | function:
66 | name: merge
67 | handler: index.handler
68 | timeout: 600
69 | memorySize: 3072
70 | runtime: python3
71 | codeUri: code/merge
72 | fc-video-demo-after-process:
73 | component: devsapp/fc
74 | props:
75 | region: ${vars.region}
76 | service: ${vars.service}
77 | function:
78 | name: after-process
79 | handler: index.handler
80 | timeout: 120
81 | memorySize: 512
82 | runtime: python3
83 | codeUri: code/after-process
84 | fc-oss-trigger-trigger-fnf:
85 | component: devsapp/fc
86 | props:
87 | region: ${vars.region}
88 | service: ${vars.service}
89 | function:
90 | name: trigger-fnf
91 | handler: index.handler
92 | timeout: 120
93 | memorySize: 128
94 | runtime: python3
95 | codeUri: code/oss-trigger
96 | environmentVariables:
97 | OUTPUT_DST: '{{ outputDir }}'
98 | FLOW_NAME: ${vars.flowName}
99 | SEG_INTERVAL: '{{ segInterval }}'
100 | DST_FORMATS: '{{ dstFormats }}'
101 | triggers:
102 | - name: oss-t
103 | type: oss
104 | role: '{{ triggerRoleArn }}'
105 | config:
106 | events:
107 | - oss:ObjectCreated:PutObject
108 | - oss:ObjectCreated:PostObject
109 | - oss:ObjectCreated:CompleteMultipartUpload
110 | filter:
111 | Key:
112 | Prefix: '{{ prefix }}'
113 | Suffix: ''
114 | bucketName: '{{ ossBucket }}'
115 |
116 | # fnf 服务配置
117 | video-demo-flow:
118 | component: devsapp/fnf
119 | props:
120 | name: ${vars.flowName}
121 | region: ${vars.region}
122 | description: FnF video processing demo flow
123 | definition: code/flows/video-processing-fc.yml
124 | roleArn: "{{ fnfRoleArn }}"
--------------------------------------------------------------------------------
/video-process-flow/version.md:
--------------------------------------------------------------------------------
1 | - 第一版
2 |
--------------------------------------------------------------------------------