├── .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 | 22 | 23 |
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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![](https://img.alicdn.com/imgextra/i3/O1CN01fbUSSP1umgrF0cfFr_!!6000000006080-2-tps-3048-1706.png) 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 | 22 | 23 | 24 | 25 |
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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![](https://img.alicdn.com/imgextra/i4/O1CN01jN5xQl1oUvle8aXFq_!!6000000005229-2-tps-1795-871.png) 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 | 22 | 23 | 24 | 25 |
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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![](https://img.alicdn.com/imgextra/i4/O1CN01jN5xQl1oUvle8aXFq_!!6000000005229-2-tps-1795-871.png) 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 | 22 | 23 | 24 | 25 |
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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![](https://img.alicdn.com/imgextra/i4/O1CN018E0DCi1X7FqlDnPSO_!!6000000002876-2-tps-1894-491.png) 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 | 22 | 23 | 24 | 25 |
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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![](https://img.alicdn.com/imgextra/i4/O1CN018E0DCi1X7FqlDnPSO_!!6000000002876-2-tps-1894-491.png) 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 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ## 部署 & 体验 32 | 33 | 34 | 35 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=PanoramicPageRecording) , 36 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ## 部署 & 体验 32 | 33 | 34 | 35 | - :fire: 通过 [Serverless 应用中心](https://fcnext.console.aliyun.com/applications/create?template=PanoramicPageRecording) , 36 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | 22 | 23 |
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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![](https://img.alicdn.com/imgextra/i3/O1CN01fbUSSP1umgrF0cfFr_!!6000000006080-2-tps-3048-1706.png) 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 | 22 | 23 |
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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![](https://img.alicdn.com/imgextra/i3/O1CN01fbUSSP1umgrF0cfFr_!!6000000006080-2-tps-3048-1706.png) 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 | 22 | 23 | 24 | 25 |
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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![](https://img.alicdn.com/imgextra/i4/O1CN01jN5xQl1oUvle8aXFq_!!6000000005229-2-tps-1795-871.png) 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 | 22 | 23 | 24 | 25 |
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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![](https://img.alicdn.com/imgextra/i4/O1CN01jN5xQl1oUvle8aXFq_!!6000000005229-2-tps-1795-871.png) 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 | [![Deploy with Severless Devs](https://img.alicdn.com/imgextra/i1/O1CN01w5RFbX1v45s8TIXPz_!!6000000006118-55-tps-95-28.svg)](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 | ![image](https://img.alicdn.com/tfs/TB1A.PSzrj1gK0jSZFuXXcrHpXa-570-613.png) 113 | 114 | 115 | 116 | ## 使用文档 117 | 118 | 119 | 120 | **操作视频教程:** 121 | 122 | [![Watch the video](https://img.alicdn.com/imgextra/i2/O1CN01XvnqJu1XLS8SAU7LT_!!6000000002907-2-tps-250-155.png)](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 | --------------------------------------------------------------------------------