├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── examples ├── animate.js ├── assets │ ├── audio │ │ ├── 01.wav │ │ ├── 02.wav │ │ └── 03.wav │ ├── font │ │ ├── jdnt.ttf │ │ ├── scsf.ttf │ │ └── ysst.ttf │ ├── gif │ │ ├── circleclose.gif │ │ ├── circlecrop.gif │ │ ├── circleopen.gif │ │ ├── diagbl.gif │ │ ├── diagbr.gif │ │ ├── diagtl.gif │ │ ├── diagtr.gif │ │ ├── dissolve.gif │ │ ├── distance.gif │ │ ├── fade.gif │ │ ├── fadeblack.gif │ │ ├── fadegrays.gif │ │ ├── fadewhite.gif │ │ ├── hblur.gif │ │ ├── hlslice.gif │ │ ├── horzclose.gif │ │ ├── horzopen.gif │ │ ├── hrslice.gif │ │ ├── pixelize.gif │ │ ├── radial.gif │ │ ├── rectcrop.gif │ │ ├── slidedown.gif │ │ ├── slideleft.gif │ │ ├── slideright.gif │ │ ├── slideup.gif │ │ ├── smoothdown.gif │ │ ├── smoothleft.gif │ │ ├── smoothright.gif │ │ ├── smoothup.gif │ │ ├── squeezeh.gif │ │ ├── squeezev.gif │ │ ├── vdslice.gif │ │ ├── vertclose.gif │ │ ├── vertopen.gif │ │ ├── vuslice.gif │ │ ├── wipebl.gif │ │ ├── wipebr.gif │ │ ├── wipedown.gif │ │ ├── wipeleft.gif │ │ ├── wiperight.gif │ │ ├── wipetl.gif │ │ ├── wipetr.gif │ │ └── wipeup.gif │ ├── imgs │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 03.png │ │ ├── 04.png │ │ ├── 05.png │ │ ├── 06.png │ │ ├── 07.png │ │ ├── demo │ │ │ ├── 03.gif │ │ │ ├── 04.gif │ │ │ └── foo.png │ │ ├── logo │ │ │ ├── logo.png │ │ │ ├── logo2.png │ │ │ └── small │ │ │ │ ├── logo1.png │ │ │ │ ├── logo2.png │ │ │ │ ├── logo3.png │ │ │ │ ├── logo4.png │ │ │ │ └── logo5.png │ │ └── wallp │ │ │ ├── 01.jpeg │ │ │ ├── 02.jpeg │ │ │ ├── 03.jpeg │ │ │ ├── 04.jpeg │ │ │ ├── 05.jpeg │ │ │ └── 06.jpeg │ └── video │ │ ├── video1.mp4 │ │ └── video2.mp4 ├── image.js ├── index.js ├── listen.js ├── live.js ├── text.js ├── transition.js └── video.js ├── index.js ├── jest.config.js ├── lib ├── animate │ ├── alpha.js │ ├── anifilter.js │ ├── animation.js │ ├── animations.js │ ├── effects.js │ ├── fade.js │ ├── move.js │ ├── rotate.js │ ├── transition.js │ └── zoom.js ├── assets │ └── blank.png ├── conf │ └── conf.js ├── core │ ├── base.js │ ├── center.js │ ├── context.js │ ├── renderer.js │ └── synthesis.js ├── creator.js ├── index.js ├── math │ ├── ease.js │ └── maths.js ├── node │ ├── audio.js │ ├── background.js │ ├── cons.js │ ├── image.js │ ├── live.js │ ├── node.js │ ├── scene.js │ ├── text.js │ └── video.js └── utils │ ├── cache.js │ ├── date.js │ ├── ffmpeg.js │ ├── filter.js │ ├── perf.js │ ├── scenes.js │ └── utils.js ├── package-lock.json ├── package.json ├── scripts ├── crop.sh ├── fade.sh ├── move-out.sh ├── move.sh ├── rotate-if.sh ├── rotate.sh ├── s+r+m.sh ├── zoom.sh └── zoomloop.sh └── test └── unit ├── conf └── conf.test.js ├── core └── base.test.js ├── node └── node.test.js └── utils └── ffmpeg.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [{*.js,*.json,*.yml}] 10 | indent_size = 2 11 | indent_style = space -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/* 2 | docs/* 3 | test/* 4 | scripts/* 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: require.resolve('babel-eslint'), 5 | ecmaVersion: 2018, 6 | sourceType: 'module', 7 | }, 8 | env: { 9 | es6: true, 10 | node: true, 11 | browser: true, 12 | }, 13 | plugins: [], 14 | extends: ['eslint:recommended'], 15 | globals: { 16 | global: true, 17 | }, 18 | rules: { 19 | 'no-useless-escape': 0, 20 | 'no-empty': 0, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run lint 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs and editors 2 | .idea 3 | .project 4 | .classpath 5 | .c9/ 6 | *.launch 7 | .settings/ 8 | *.sublime-workspace 9 | 10 | # IDE - VSCode 11 | .vscode/* 12 | !.vscode/settings.json 13 | !.vscode/tasks.json 14 | !.vscode/launch.json 15 | !.vscode/extensions.json 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # next.js build output 25 | .next 26 | 27 | # Lerna 28 | lerna-debug.log 29 | 30 | # System Files 31 | .DS_Store 32 | Thumbs.db 33 | 34 | # node modules 35 | node_modules 36 | 37 | # documentation 38 | build/docs/ 39 | 40 | # package-lock.json 41 | .DS_Store 42 | source/build/ 43 | static/ 44 | examples/cache 45 | examples/output 46 | 47 | # testing 48 | coverage/** 49 | dwt* 50 | /coverage/ 51 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | /scripts 7 | 8 | # misc 9 | .DS_Store 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | .code.yml 15 | 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | /docs 21 | /test 22 | /logo 23 | /examples 24 | lib/.DS_Store 25 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | semi: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | bracketSpacing: true, 7 | jsxBracketSameLine: true, 8 | arrowParens: 'avoid', 9 | insertPragma: false, 10 | tabWidth: 2, 11 | useTabs: false, 12 | }; 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | script: 5 | - npm run travis 6 | branches: 7 | only: 8 | - master -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 为 FFCreator 做出贡献 2 | 3 | 欢迎您 [提出问题](https://github.com/tnfe/FFCreatorLite/issues) 或 [merge requests](https://github.com/tnfe/FFCreatorLite/merge_requests), 建议您在为 FFCreator 做出贡献前先阅读以下 FFCreator 贡献指南。 4 | 5 | ## issues 6 | 7 | 我们通过 [issues](https://github.com/tnfe/FFCreatorLite/issues) 来收集问题和功能相关的需求。 8 | 9 | ### 首先查看已知的问题 10 | 11 | 在您准备提出问题以前,请先查看现有的 [issues](https://github.com/tnfe/FFCreatorLite/issues) 是否已有其他人提出过相似的功能或问题,以确保您提出的问题是有效的。 12 | 13 | ### 提交问题 14 | 15 | 问题的表述应当尽可能的详细,可以包含相关的代码块。 16 | 17 | ## Merge Requests 18 | 19 | 我们十分期待您通过 [Merge Requests](https://github.com/tnfe/FFCreatorLite/merge_requests) 让 FFCreator 变的更加完善。 20 | 21 | ### 分支管理 22 | 23 | FFCreator 主仓库只包含 master 分支,其将作为稳定的开发分支,经过测试后会打 Tag 进行发布。 24 | 25 | ### Commit Message 26 | 27 | 我们希望您能使用`npm run commit`来提交代码,保持项目的一致性。 28 | 这样可以方便生成每个版本的 Changelog,很容易地追溯历史。 29 | 30 | ### MR 流程 31 | 32 | TNFE 团队会查看所有的 MR,我们会运行一些代码检查和测试,一经测试通过,我们会接受这次 MR,但不会立即发布外网,会有一些延迟。 33 | 34 | 当您准备 MR 时,请确保已经完成以下几个步骤: 35 | 36 | 1. 将主仓库代码 Fork 到自己名下。 37 | 2. 基于 `master` 分支创建您的开发分支。 38 | 3. 如果您更改了 API(s) 请更新代码及文档。 39 | 4. 检查您的代码语法及格式。 40 | 5. 提一个 MR 到主仓库的 `master` 分支上。 41 | 42 | ### 本地开发 43 | 44 | 首先安装相关依赖 45 | 46 | ```bash 47 | npm i 48 | ``` 49 | 50 | 运行 examples 下相关demo 51 | 52 | ```bash 53 | npm run examples 54 | ``` 55 | 56 | 使用 [npm link](https://docs.npmjs.com/cli/link.html) 进行测试 57 | 58 | ```bash 59 | npm link 60 | ``` 61 | 62 | ## 许可证 63 | 64 | 通过为 FFCreator 做出贡献,代表您同意将其版权归为 FFCreator 所有,开源协议为 [MIT LICENSE](https://opensource.org/licenses/MIT) 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) FFCreator authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/animate.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const colors = require('colors'); 3 | const startAndListen = require('./listen'); 4 | const {FFCreatorCenter, FFScene, FFImage, FFText, FFCreator} = require('../'); 5 | 6 | const createFFTask = () => { 7 | const img1 = path.join(__dirname, './assets/imgs/04.png'); 8 | const img2 = path.join(__dirname, './assets/imgs/05.png'); 9 | const img3 = path.join(__dirname, './assets/imgs/06.png'); 10 | const img4 = path.join(__dirname, './assets/imgs/07.png'); 11 | const bg1 = path.join(__dirname, './assets/imgs/wallp/03.jpeg'); 12 | const bg2 = path.join(__dirname, './assets/imgs/wallp/02.jpeg'); 13 | const logo = path.join(__dirname, './assets/imgs/logo/logo2.png'); 14 | const font = path.join(__dirname, './assets/font/scsf.ttf'); 15 | const audio = path.join(__dirname, './assets/audio/03.wav'); 16 | const cacheDir = path.join(__dirname, './cache/'); 17 | const outputDir = path.join(__dirname, './output/'); 18 | 19 | // create creator instance 20 | const creator = new FFCreator({ 21 | cacheDir, 22 | outputDir, 23 | width: 576, 24 | height: 1024, 25 | log: true, 26 | audio, 27 | }); 28 | 29 | // create FFScene 30 | const scene1 = new FFScene(); 31 | const scene2 = new FFScene(); 32 | scene1.setBgColor('#b53471'); 33 | scene2.setBgColor('#0752dd'); 34 | 35 | // scene1 36 | const fbg = new FFImage({path: bg1, y: 300}); 37 | scene1.addChild(fbg); 38 | 39 | const fimg2 = new FFImage({path: img2, x: 20, y: 420}); 40 | fimg2.addEffect('zoomNopadIn', 1.5, 0); 41 | scene1.addChild(fimg2); 42 | 43 | const fimg3 = new FFImage({path: img3, x: 300, y: 460}); 44 | fimg3.setScale(0.7); 45 | fimg3.addEffect('rotateInBig', 2.5, 3.5); 46 | scene1.addChild(fimg3); 47 | 48 | const fimg4 = new FFImage({path: img4, x: 60, y: 170}); 49 | fimg4.addEffect('zoomInDown', 1.8, 5.5); 50 | scene1.addChild(fimg4); 51 | 52 | const fimg1 = new FFImage({path: img1}); 53 | fimg1.addAnimate({ 54 | type: 'move', 55 | showType: 'in', 56 | time: 2, 57 | delay: 2, 58 | from: {x: 520, y: 120}, 59 | to: {x: 320, y: 220}, 60 | }); 61 | scene1.addChild(fimg1); 62 | 63 | const text1 = new FFText({text: 'FFCreatorLite动画效果', font, x: 40, y: 100, fontSize: 42}); 64 | text1.setColor('#ffffff'); 65 | text1.setBorder(5, '#000000'); 66 | text1.addEffect('fadeIn', 2, 1); 67 | scene1.addChild(text1); 68 | 69 | scene1.setDuration(12); 70 | creator.addChild(scene1); 71 | 72 | // scene2 73 | const fbg2 = new FFImage({path: bg2, y: 300}); 74 | fbg2.addEffect('zoomIn', 1.2, 0.1); 75 | scene2.addChild(fbg2); 76 | // logo 77 | const flogo = new FFImage({path: logo, x: 150, y: 170}); 78 | flogo.setScale(0.75); 79 | flogo.addEffect('moveInRight', 2, 0.3); 80 | scene2.addChild(flogo); 81 | 82 | const text2 = new FFText({text: '支持多种自定义动画', font, x: 40, y: 100, fontSize: 42}); 83 | text2.setColor('#ffc310'); 84 | text2.setBorder(5, '#000000'); 85 | text2.addEffect('moveInLeft', 2, 1); 86 | scene2.addChild(text2); 87 | 88 | const fimg5 = new FFImage({path: img3, x: 220, y: 420}); 89 | fimg5.setScale(0.7); 90 | fimg5.addEffect('rotateInBig', 2.5, 1.5); 91 | scene2.addChild(fimg5); 92 | 93 | scene2.setDuration(6); 94 | creator.addChild(scene2); 95 | 96 | creator.start(); 97 | creator.openLog(); 98 | 99 | creator.on('start', () => { 100 | console.log(`FFCreatorLite start`); 101 | }); 102 | 103 | creator.on('error', e => { 104 | console.log(`FFCreatorLite error:: \n ${e}`); 105 | }); 106 | 107 | creator.on('progress', e => { 108 | console.log(colors.yellow(`FFCreatorLite progress: ${(e.percent * 100) >> 0}%`)); 109 | }); 110 | 111 | creator.on('complete', e => { 112 | console.log( 113 | colors.magenta(`FFCreatorLite completed: \n USEAGE: ${e.useage} \n PATH: ${e.output} `), 114 | ); 115 | 116 | console.log(colors.green(`\n --- You can press the s key or the w key to restart! --- \n`)); 117 | }); 118 | 119 | return creator; 120 | }; 121 | 122 | module.exports = () => startAndListen(() => FFCreatorCenter.addTask(createFFTask)); 123 | -------------------------------------------------------------------------------- /examples/assets/audio/01.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/audio/01.wav -------------------------------------------------------------------------------- /examples/assets/audio/02.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/audio/02.wav -------------------------------------------------------------------------------- /examples/assets/audio/03.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/audio/03.wav -------------------------------------------------------------------------------- /examples/assets/font/jdnt.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/font/jdnt.ttf -------------------------------------------------------------------------------- /examples/assets/font/scsf.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/font/scsf.ttf -------------------------------------------------------------------------------- /examples/assets/font/ysst.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/font/ysst.ttf -------------------------------------------------------------------------------- /examples/assets/gif/circleclose.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/circleclose.gif -------------------------------------------------------------------------------- /examples/assets/gif/circlecrop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/circlecrop.gif -------------------------------------------------------------------------------- /examples/assets/gif/circleopen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/circleopen.gif -------------------------------------------------------------------------------- /examples/assets/gif/diagbl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/diagbl.gif -------------------------------------------------------------------------------- /examples/assets/gif/diagbr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/diagbr.gif -------------------------------------------------------------------------------- /examples/assets/gif/diagtl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/diagtl.gif -------------------------------------------------------------------------------- /examples/assets/gif/diagtr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/diagtr.gif -------------------------------------------------------------------------------- /examples/assets/gif/dissolve.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/dissolve.gif -------------------------------------------------------------------------------- /examples/assets/gif/distance.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/distance.gif -------------------------------------------------------------------------------- /examples/assets/gif/fade.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/fade.gif -------------------------------------------------------------------------------- /examples/assets/gif/fadeblack.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/fadeblack.gif -------------------------------------------------------------------------------- /examples/assets/gif/fadegrays.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/fadegrays.gif -------------------------------------------------------------------------------- /examples/assets/gif/fadewhite.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/fadewhite.gif -------------------------------------------------------------------------------- /examples/assets/gif/hblur.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/hblur.gif -------------------------------------------------------------------------------- /examples/assets/gif/hlslice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/hlslice.gif -------------------------------------------------------------------------------- /examples/assets/gif/horzclose.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/horzclose.gif -------------------------------------------------------------------------------- /examples/assets/gif/horzopen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/horzopen.gif -------------------------------------------------------------------------------- /examples/assets/gif/hrslice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/hrslice.gif -------------------------------------------------------------------------------- /examples/assets/gif/pixelize.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/pixelize.gif -------------------------------------------------------------------------------- /examples/assets/gif/radial.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/radial.gif -------------------------------------------------------------------------------- /examples/assets/gif/rectcrop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/rectcrop.gif -------------------------------------------------------------------------------- /examples/assets/gif/slidedown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/slidedown.gif -------------------------------------------------------------------------------- /examples/assets/gif/slideleft.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/slideleft.gif -------------------------------------------------------------------------------- /examples/assets/gif/slideright.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/slideright.gif -------------------------------------------------------------------------------- /examples/assets/gif/slideup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/slideup.gif -------------------------------------------------------------------------------- /examples/assets/gif/smoothdown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/smoothdown.gif -------------------------------------------------------------------------------- /examples/assets/gif/smoothleft.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/smoothleft.gif -------------------------------------------------------------------------------- /examples/assets/gif/smoothright.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/smoothright.gif -------------------------------------------------------------------------------- /examples/assets/gif/smoothup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/smoothup.gif -------------------------------------------------------------------------------- /examples/assets/gif/squeezeh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/squeezeh.gif -------------------------------------------------------------------------------- /examples/assets/gif/squeezev.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/squeezev.gif -------------------------------------------------------------------------------- /examples/assets/gif/vdslice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/vdslice.gif -------------------------------------------------------------------------------- /examples/assets/gif/vertclose.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/vertclose.gif -------------------------------------------------------------------------------- /examples/assets/gif/vertopen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/vertopen.gif -------------------------------------------------------------------------------- /examples/assets/gif/vuslice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/vuslice.gif -------------------------------------------------------------------------------- /examples/assets/gif/wipebl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/wipebl.gif -------------------------------------------------------------------------------- /examples/assets/gif/wipebr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/wipebr.gif -------------------------------------------------------------------------------- /examples/assets/gif/wipedown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/wipedown.gif -------------------------------------------------------------------------------- /examples/assets/gif/wipeleft.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/wipeleft.gif -------------------------------------------------------------------------------- /examples/assets/gif/wiperight.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/wiperight.gif -------------------------------------------------------------------------------- /examples/assets/gif/wipetl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/wipetl.gif -------------------------------------------------------------------------------- /examples/assets/gif/wipetr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/wipetr.gif -------------------------------------------------------------------------------- /examples/assets/gif/wipeup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/gif/wipeup.gif -------------------------------------------------------------------------------- /examples/assets/imgs/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/01.png -------------------------------------------------------------------------------- /examples/assets/imgs/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/02.png -------------------------------------------------------------------------------- /examples/assets/imgs/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/03.png -------------------------------------------------------------------------------- /examples/assets/imgs/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/04.png -------------------------------------------------------------------------------- /examples/assets/imgs/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/05.png -------------------------------------------------------------------------------- /examples/assets/imgs/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/06.png -------------------------------------------------------------------------------- /examples/assets/imgs/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/07.png -------------------------------------------------------------------------------- /examples/assets/imgs/demo/03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/demo/03.gif -------------------------------------------------------------------------------- /examples/assets/imgs/demo/04.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/demo/04.gif -------------------------------------------------------------------------------- /examples/assets/imgs/demo/foo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/demo/foo.png -------------------------------------------------------------------------------- /examples/assets/imgs/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/logo/logo.png -------------------------------------------------------------------------------- /examples/assets/imgs/logo/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/logo/logo2.png -------------------------------------------------------------------------------- /examples/assets/imgs/logo/small/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/logo/small/logo1.png -------------------------------------------------------------------------------- /examples/assets/imgs/logo/small/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/logo/small/logo2.png -------------------------------------------------------------------------------- /examples/assets/imgs/logo/small/logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/logo/small/logo3.png -------------------------------------------------------------------------------- /examples/assets/imgs/logo/small/logo4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/logo/small/logo4.png -------------------------------------------------------------------------------- /examples/assets/imgs/logo/small/logo5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/logo/small/logo5.png -------------------------------------------------------------------------------- /examples/assets/imgs/wallp/01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/wallp/01.jpeg -------------------------------------------------------------------------------- /examples/assets/imgs/wallp/02.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/wallp/02.jpeg -------------------------------------------------------------------------------- /examples/assets/imgs/wallp/03.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/wallp/03.jpeg -------------------------------------------------------------------------------- /examples/assets/imgs/wallp/04.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/wallp/04.jpeg -------------------------------------------------------------------------------- /examples/assets/imgs/wallp/05.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/wallp/05.jpeg -------------------------------------------------------------------------------- /examples/assets/imgs/wallp/06.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/imgs/wallp/06.jpeg -------------------------------------------------------------------------------- /examples/assets/video/video1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/video/video1.mp4 -------------------------------------------------------------------------------- /examples/assets/video/video2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/examples/assets/video/video2.mp4 -------------------------------------------------------------------------------- /examples/image.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const colors = require('colors'); 3 | const startAndListen = require('./listen'); 4 | const { FFCreatorCenter, FFScene, FFImage, FFText, FFCreator } = require('../'); 5 | 6 | const createImageAnimation = () => { 7 | const img1 = path.join(__dirname, './assets/imgs/01.png'); 8 | const img2 = path.join(__dirname, './assets/imgs/02.png'); 9 | const img3 = path.join(__dirname, './assets/imgs/03.png'); 10 | const bg1 = path.join(__dirname, './assets/imgs/wallp/06.jpeg'); 11 | const bg2 = path.join(__dirname, './assets/imgs/wallp/01.jpeg'); 12 | const logo = path.join(__dirname, './assets/imgs/logo/logo2.png'); 13 | const font = path.join(__dirname, './assets/font/ysst.ttf'); 14 | const audio = path.join(__dirname, './assets/audio/01.wav'); 15 | const cacheDir = path.join(__dirname, './cache/'); 16 | const outputDir = path.join(__dirname, './output/'); 17 | 18 | // create creator instance 19 | const creator = new FFCreator({ 20 | cacheDir, 21 | outputDir, 22 | width: 600, 23 | height: 400, 24 | log: true, 25 | //debug: true, 26 | audio, 27 | }); 28 | 29 | // create FFScene 30 | const scene1 = new FFScene(); 31 | const scene2 = new FFScene(); 32 | scene1.setBgColor('#ff0000'); 33 | scene2.setBgColor('#b33771'); 34 | 35 | // scene1 36 | const fbg = new FFImage({ path: bg1 }); 37 | scene1.addChild(fbg); 38 | 39 | const fimg1 = new FFImage({ path: img1, x: 300, y: 60 }); 40 | fimg1.addEffect('moveInRight', 1.5, 1.2); 41 | scene1.addChild(fimg1); 42 | 43 | const fimg2 = new FFImage({ path: img2, x: 20, y: 80 }); 44 | fimg2.addEffect('moveInLeft', 1.5, 0); 45 | scene1.addChild(fimg2); 46 | 47 | const fimg3 = new FFImage({ path: img3, x: 200, y: 170 }); 48 | fimg3.addEffect('rotateInBig', 2.5, 3.5); 49 | scene1.addChild(fimg3); 50 | 51 | const text1 = new FFText({ text: '这是第一屏', font, x: 220, y: 30, fontSize: 36 }); 52 | text1.setColor('#ffffff'); 53 | text1.setBackgroundColor('#000000'); 54 | text1.addEffect('fadeIn', 1, 1); 55 | scene1.addChild(text1); 56 | 57 | scene1.setDuration(8); 58 | creator.addChild(scene1); 59 | 60 | // scene2 61 | const fbg2 = new FFImage({ path: bg2 }); 62 | fbg2.addEffect('zoomIn', 0.5, 0); 63 | scene2.addChild(fbg2); 64 | // logo 65 | const flogo = new FFImage({ path: logo, x: 120, y: 170 }); 66 | flogo.setScale(0.75); 67 | flogo.addEffect('moveInUpBack', 1.2, 0.3); 68 | scene2.addChild(flogo); 69 | 70 | const text2 = new FFText({ text: '这是第二屏', font, x: 220, y: 30, fontSize: 36 }); 71 | text2.setColor('#ffffff'); 72 | text2.setBackgroundColor('#000000'); 73 | text2.addEffect('fadeIn', 1, 0.3); 74 | scene2.addChild(text2); 75 | 76 | scene2.setDuration(6); 77 | creator.addChild(scene2); 78 | 79 | creator.start(); 80 | creator.openLog(); 81 | 82 | creator.on('start', () => { 83 | console.log(`FFCreatorLite start`); 84 | }); 85 | 86 | creator.on('error', e => { 87 | console.log(`FFCreatorLite error:: \n ${JSON.stringify(e)}`); 88 | }); 89 | 90 | creator.on('progress', e => { 91 | console.log(colors.yellow(`FFCreatorLite progress: ${(e.percent * 100) >> 0}%`)); 92 | }); 93 | 94 | creator.on('complete', e => { 95 | console.log( 96 | colors.magenta(`FFCreatorLite completed: \n USEAGE: ${e.useage} \n PATH: ${e.output} `), 97 | ); 98 | 99 | console.log(colors.green(`\n --- You can press the s key or the w key to restart! --- \n`)); 100 | }); 101 | 102 | return creator; 103 | }; 104 | 105 | module.exports = () => startAndListen(() => FFCreatorCenter.addTask(createImageAnimation)); 106 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const colors = require('colors'); 4 | const inquirer = require('inquirer'); 5 | 6 | const printRestartInfo = () => 7 | console.log(colors.green(`\n --- You can press the s key or the w key to restart! --- \n`)); 8 | 9 | const clearAllFiles = () => { 10 | fs.remove(path.join(__dirname, './output')); 11 | fs.remove(path.join(__dirname, './cache')); 12 | }; 13 | 14 | const choices = [ 15 | { 16 | name: 'Picture animation video', 17 | value: 'image', 18 | }, 19 | { 20 | name: 'Multiple text combinations', 21 | value: 'text', 22 | }, 23 | { 24 | name: 'Animation effect display', 25 | value: 'animate', 26 | }, 27 | { 28 | name: 'Scene transition effect', 29 | value: 'transition', 30 | }, 31 | { 32 | name: 'Video animation demo', 33 | value: 'video', 34 | }, 35 | { 36 | name: 'Push live rtmp stream', 37 | value: 'live', 38 | }, 39 | { 40 | name: 'Clear all caches and videos', 41 | value: 'clear', 42 | func: clearAllFiles, 43 | }, 44 | new inquirer.Separator(), 45 | ]; 46 | 47 | const runDemo = answer => { 48 | for (let i = 0; i < choices.length; i++) { 49 | const choice = choices[i]; 50 | if (choice.value === answer.val) { 51 | if (answer.val !== 'clear') printRestartInfo(); 52 | choice.func(); 53 | break; 54 | } 55 | } 56 | }; 57 | 58 | const initCommand = () => { 59 | for (let i = 0; i < choices.length; i++) { 60 | const choice = choices[i]; 61 | choice.name = `(${i + 1}) ${choice.name}`; 62 | if (choice.type !== 'separator' && choice.value) 63 | choice.func = choice.func || require(path.join(__dirname, `./${choice.value}`)); 64 | } 65 | 66 | inquirer 67 | .prompt([ 68 | { 69 | type: 'list', 70 | message: 'Please select the demo you want to run:', 71 | name: 'val', 72 | choices, 73 | pageSize: choices.length, 74 | validate: function(answer) { 75 | if (answer.length < 1) { 76 | return 'You must choose at least one topping.'; 77 | } 78 | return true; 79 | }, 80 | }, 81 | ]) 82 | .then(runDemo); 83 | }; 84 | 85 | initCommand(); 86 | -------------------------------------------------------------------------------- /examples/listen.js: -------------------------------------------------------------------------------- 1 | const keypress = require('keypress'); 2 | 3 | const startAndListen = func => { 4 | keypress(process.stdin); 5 | 6 | process.stdin.on('keypress', function(ch, key) { 7 | console.log('got "keypress"', key); 8 | if (key && (key.name === 's' || key.name === 'w')) { 9 | func(); 10 | } 11 | 12 | if (key && (key.name === 'q' || (key.name === 'c' && key.ctrl === true))) { 13 | process.exit(); 14 | } 15 | }); 16 | process.stdin.setRawMode(true); 17 | process.stdin.resume(); 18 | 19 | func(); 20 | }; 21 | 22 | module.exports = startAndListen; 23 | -------------------------------------------------------------------------------- /examples/live.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const colors = require('colors'); 3 | const startAndListen = require('./listen'); 4 | const { FFCreatorCenter, FFScene, FFImage, FFText, FFLive, FFCreator } = require('../'); 5 | 6 | const createFFTask = () => { 7 | const img1 = path.join(__dirname, './assets/imgs/06.png'); 8 | const bg1 = path.join(__dirname, './assets/imgs/wallp/06.jpeg'); 9 | const logo = path.join(__dirname, './assets/imgs/logo/logo2.png'); 10 | const font = path.join(__dirname, './assets/font/scsf.ttf'); 11 | const audio = path.join(__dirname, './assets/audio/03.wav'); 12 | 13 | console.log('Please enter the correct live stream.'); 14 | const live = 'rtmp://server/live/originalStream'; 15 | const output = 'rtmp://server/live/h264Stream'; 16 | 17 | const cacheDir = path.join(__dirname, './cache/'); 18 | const outputDir = path.join(__dirname, './output/'); 19 | 20 | const width = 1920; 21 | const height = 1080; 22 | // create creator instance 23 | const creator = new FFCreator({ 24 | cacheDir, 25 | output, 26 | outputDir, 27 | width, 28 | height, 29 | log: true, 30 | preset: 'veryfast', 31 | vprofile: 'baseline', 32 | upStreaming: true, 33 | audio, 34 | }); 35 | 36 | // create FFScene 37 | const scene = new FFScene(); 38 | scene.setBgColor('#9980fa'); 39 | 40 | const fbg = new FFImage({ path: bg1 }); 41 | fbg.setXY(50, 50); 42 | scene.addChild(fbg); 43 | 44 | const fflive = new FFLive({ path: live, x: 0, y: 0 }); 45 | fflive.setScale(1); 46 | fflive.addEffect('moveInRight', 2.5, 3.5); 47 | scene.addChild(fflive); 48 | 49 | const fimg1 = new FFImage({ path: img1, x: -80, y: 80 }); 50 | fimg1.addEffect('moveInLeft', 1.5, 0); 51 | scene.addChild(fimg1); 52 | 53 | const text1 = new FFText({ text: 'FFLive案例', font, x: width / 2 - 120, y: 100, fontSize: 42 }); 54 | text1.setColor('#ffffff'); 55 | text1.setBorder(5, '#000000'); 56 | scene.addChild(text1); 57 | 58 | scene.setDuration(17); 59 | creator.addChild(scene); 60 | 61 | creator.start(); 62 | creator.openLog(); 63 | 64 | creator.on('start', () => { 65 | console.log(`FFCreatorLite start`); 66 | }); 67 | 68 | creator.on('error', e => { 69 | console.log(`FFCreatorLite error:: \n ${e.error}`); 70 | }); 71 | 72 | creator.on('progress', e => { 73 | console.log(colors.yellow(`FFCreatorLite progress: ${(e.percent * 100) >> 0}%`)); 74 | }); 75 | 76 | creator.on('complete', e => { 77 | console.log( 78 | colors.magenta(`FFCreatorLite completed: \n USEAGE: ${e.useage} \n PATH: ${e.output} `), 79 | ); 80 | 81 | console.log(colors.green(`\n --- You can press the s key or the w key to restart! --- \n`)); 82 | }); 83 | 84 | return creator; 85 | }; 86 | 87 | module.exports = () => startAndListen(() => FFCreatorCenter.addTask(createFFTask)); 88 | -------------------------------------------------------------------------------- /examples/text.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const colors = require('colors'); 3 | const startAndListen = require('./listen'); 4 | const { FFCreatorCenter, FFScene, FFText, FFImage, FFCreator } = require('../'); 5 | 6 | const idiom = `风驰电掣、健步如飞、大步流星、白驹过隙、快马加鞭、眼疾手快、速战速决、转瞬即逝、语重心长、一心一意、急中生智、飞蛾扑火、金蝉脱壳、积蚊成雷、蟾宫折桂、蚕食鲸吞、蜻蜓点水、螳臂挡车、蛛丝马迹、螳螂捕蝉、黄雀在后`.split( 7 | '、', 8 | ); 9 | 10 | const getRandomColor = () => { 11 | let letters = '0123456789ABCDEF'; 12 | let color = '#'; 13 | for (let i = 0; i < 6; i++) color += letters[Math.floor(Math.random() * 16)]; 14 | return color; 15 | }; 16 | 17 | const ffcreateTask = () => { 18 | const logo = path.join(__dirname, './assets/imgs/logo/logo2.png'); 19 | const bg1 = path.join(__dirname, './assets/imgs/wallp/02.jpeg'); 20 | const bg2 = path.join(__dirname, './assets/imgs/wallp/04.jpeg'); 21 | const font1 = path.join(__dirname, './assets/font/ysst.ttf'); 22 | const font2 = path.join(__dirname, './assets/font/jdnt.ttf'); 23 | const audio = path.join(__dirname, './assets/audio/02.wav'); 24 | const outputDir = path.join(__dirname, './output/'); 25 | const cacheDir = path.join(__dirname, './cache/'); 26 | 27 | // create creator instance 28 | const creator = new FFCreator({ 29 | cacheDir, 30 | outputDir, 31 | width: 600, 32 | height: 400, 33 | debug: false, 34 | audio, 35 | }); 36 | 37 | // create FFScene 38 | const scene1 = new FFScene(); 39 | scene1.addChild(new FFImage({ path: bg1 })); 40 | 41 | // 多个文字 42 | for (let i = 0; i < 50; i++) { 43 | const font = i % 2 === 0 ? font1 : font2; 44 | const effect = i % 2 === 0 ? 'moveInLeftBig' : 'moveInRightBig'; 45 | const x = (-20 + Math.random() * 500) >> 0; 46 | const y = (10 + Math.random() * 350) >> 0; 47 | const time = (2 + Math.random() * 8) >> 0; 48 | const delay = ((Math.random() * 10 * 10) >> 0) / 10; 49 | const fontSize = (16 + Math.random() * 120) >> 0; 50 | const text1 = new FFText({ text: idiom[i % idiom.length], font, fontSize }); 51 | text1.setXY(x, y); 52 | text1.setColor(getRandomColor()); 53 | text1.addEffect(effect, time, delay); 54 | scene1.addChild(text1); 55 | } 56 | scene1.setDuration(15); 57 | scene1.setTransition('radial', 1.2); 58 | creator.addChild(scene1); 59 | 60 | // scene2 61 | const scene2 = new FFScene(); 62 | scene2.setBgColor('#b33771'); 63 | const fbg2 = new FFImage({ path: bg2 }); 64 | fbg2.addEffect('rotateInBig', 1.5, 0); 65 | scene2.addChild(fbg2); 66 | 67 | // logo 68 | const flogo = new FFImage({ path: logo, x: 150, y: 180 }); 69 | flogo.setScale(0.7); 70 | flogo.addEffect('moveInUpBack', 1, 2); 71 | scene2.addChild(flogo); 72 | 73 | scene2.setDuration(5); 74 | creator.addChild(scene2); 75 | 76 | creator.start(); 77 | // creator.openLog(); 78 | 79 | creator.on('start', () => { 80 | console.log(`FFCreator start`); 81 | }); 82 | 83 | creator.on('error', e => { 84 | console.log(`FFCreator error: ${e.error}}`); 85 | }); 86 | 87 | creator.on('progress', e => { 88 | console.log(colors.yellow(`FFCreator progress: ${(e.percent * 100) >> 0}%`)); 89 | }); 90 | 91 | creator.on('complete', e => { 92 | console.log( 93 | colors.magenta(`FFCreator completed: \n USEAGE: ${e.useage} \n PATH: ${e.output} `), 94 | ); 95 | 96 | console.log(colors.green(`\n --- You can press the s key or the w key to restart! --- \n`)); 97 | }); 98 | 99 | return creator; 100 | }; 101 | 102 | module.exports = () => startAndListen(() => FFCreatorCenter.addTask(ffcreateTask)); 103 | -------------------------------------------------------------------------------- /examples/transition.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const colors = require('colors'); 3 | const shuffle = require('lodash/shuffle'); 4 | const startAndListen = require('./listen'); 5 | const { FFCreatorCenter, FFScene, FFText, FFImage, FFCreator } = require('../'); 6 | 7 | const width = 600; 8 | const height = 400; 9 | const font = path.join(__dirname, './assets/font/ysst.ttf'); 10 | const audio = path.join(__dirname, './assets/audio/01.wav'); 11 | const outputDir = path.join(__dirname, './output/'); 12 | const cacheDir = path.join(__dirname, './cache/'); 13 | 14 | const transitionDemoTask = () => { 15 | const trans = shuffle(['pixelize', 'circleclose', 'slideup', 'hrslice', 'wipetl']); 16 | const order = ['一', '二', '三', '四', '五']; 17 | 18 | // create creator instance 19 | const creator = new FFCreator({ 20 | cacheDir, 21 | outputDir, 22 | width, 23 | height, 24 | audio, 25 | debug: false, 26 | }); 27 | 28 | for (let i = 1; i < 6; i++) { 29 | const transition = trans[i - 1]; 30 | const text = `这是第 ${order[i - 1]} 屏`; 31 | const scene = creatScene({ index: i, transition, text }); 32 | creator.addChild(scene); 33 | } 34 | 35 | creator.openLog(); 36 | creator.start(); 37 | 38 | creator.on('start', () => { 39 | console.log(`FFCreator start`); 40 | }); 41 | 42 | creator.on('error', e => { 43 | console.log(e); 44 | }); 45 | 46 | creator.on('progress', e => { 47 | // console.log(colors.yellow(`FFCreator progress: ${(e.percent * 100) >> 0}%`)); 48 | }); 49 | 50 | creator.on('complete', e => { 51 | console.log( 52 | colors.magenta(`FFCreator completed: \n USEAGE: ${e.useage} \n PATH: ${e.output} `), 53 | ); 54 | 55 | console.log(colors.green(`\n --- You can press the s key or the w key to restart! --- \n`)); 56 | }); 57 | 58 | return creator; 59 | }; 60 | 61 | const creatScene = ({ index, transition, text }) => { 62 | const scene = new FFScene(); 63 | scene.setBgColor('#3b3a98'); 64 | scene.setDuration(5); 65 | scene.setTransition(transition, 1.5); 66 | 67 | // bg img 68 | const img = path.join(__dirname, `./assets/imgs/wallp/0${index}.jpeg`); 69 | const bg = new FFImage({ path: img, resetXY: true }); 70 | scene.addChild(bg); 71 | 72 | // title text 73 | const ftext = new FFText({ text, x: width / 2 - 100, y: height / 2 + 50, font, fontSize: 38 }); 74 | ftext.setColor('#30336b'); 75 | ftext.setBackgroundColor('#ffffff'); 76 | ftext.addEffect('moveInUpBack', 1, 1.3); 77 | scene.addChild(ftext); 78 | 79 | // add logo2 80 | const scale = 1; 81 | const logo = path.join(__dirname, `./assets/imgs/logo/small/logo${index}.png`); 82 | const flogo = new FFImage({ path: logo, x: width / 2 - (520 * scale) / 2, y: height / 2 - 100 }); 83 | //flogo.setScale(scale); 84 | flogo.addEffect('moveInUp', 1, 1.8); 85 | scene.addChild(flogo); 86 | 87 | return scene; 88 | }; 89 | 90 | module.exports = () => startAndListen(() => FFCreatorCenter.addTask(transitionDemoTask)); 91 | -------------------------------------------------------------------------------- /examples/video.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const colors = require('colors'); 3 | const startAndListen = require('./listen'); 4 | const { FFCreatorCenter, FFScene, FFImage, FFText, FFVideo, FFCreator } = require('../'); 5 | 6 | const createFFTask = () => { 7 | const img3 = path.join(__dirname, './assets/imgs/06.png'); 8 | const logo = path.join(__dirname, './assets/imgs/logo/logo2.png'); 9 | const font = path.join(__dirname, './assets/font/scsf.ttf'); 10 | const audio = path.join(__dirname, './assets/audio/03.wav'); 11 | const video1 = path.join(__dirname, './assets/video/video1.mp4'); 12 | const video2 = path.join(__dirname, './assets/video/video2.mp4'); 13 | 14 | const cacheDir = path.join(__dirname, './cache/'); 15 | const outputDir = path.join(__dirname, './output/'); 16 | 17 | // create creator instance 18 | const creator = new FFCreator({ 19 | cacheDir, 20 | outputDir, 21 | width: 576, 22 | height: 1024, 23 | log: true, 24 | //audio, 25 | }); 26 | 27 | // create FFScene 28 | const scene1 = new FFScene(); 29 | const scene2 = new FFScene(); 30 | scene1.setBgColor('#9980fa'); 31 | scene2.setBgColor('#ea2228'); 32 | 33 | const fvideo1 = new FFVideo({ path: video1, audio: true, y: 330 }); 34 | fvideo1.setScale(0.6); 35 | scene1.addChild(fvideo1); 36 | 37 | const fvideo2 = new FFVideo({ path: video2, audio: false, x: 300, y: 330 }); 38 | fvideo2.setScale(0.3); 39 | fvideo2.addEffect('moveInRight', 2.5, 3.5); 40 | scene1.addChild(fvideo2); 41 | 42 | const fimg3 = new FFImage({ path: img3, x: 60, y: 600 }); 43 | fimg3.setScale(0.4); 44 | fimg3.addEffect('rotateInBig', 2.5, 1.5); 45 | scene1.addChild(fimg3); 46 | 47 | const text1 = new FFText({ text: 'FFVideo案例', font, x: 140, y: 100, fontSize: 42 }); 48 | text1.setColor('#ffffff'); 49 | text1.setBorder(5, '#000000'); 50 | //text1.addEffect('fadeIn', 2, 1); 51 | scene1.addChild(text1); 52 | 53 | scene1.setDuration(17); 54 | creator.addChild(scene1); 55 | 56 | creator.start(); 57 | creator.openLog(); 58 | 59 | creator.on('start', () => { 60 | console.log(`FFCreatorLite start`); 61 | }); 62 | 63 | creator.on('error', e => { 64 | console.log(`FFCreatorLite error:: \n ${e.error}`); 65 | }); 66 | 67 | creator.on('progress', e => { 68 | console.log(colors.yellow(`FFCreatorLite progress: ${(e.percent * 100) >> 0}%`)); 69 | }); 70 | 71 | creator.on('complete', e => { 72 | console.log( 73 | colors.magenta(`FFCreatorLite completed: \n USEAGE: ${e.useage} \n PATH: ${e.output} `), 74 | ); 75 | 76 | console.log(colors.green(`\n --- You can press the s key or the w key to restart! --- \n`)); 77 | }); 78 | 79 | return creator; 80 | }; 81 | 82 | module.exports = () => startAndListen(() => FFCreatorCenter.addTask(createFFTask)); 83 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib'); 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/fg/3ckg0td56pvcq9vydnq6wj_00000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: true, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | collectCoverageFrom: [ 25 | '/lib/**/*.js', 26 | ], 27 | 28 | moduleNameMapper: { 29 | '@/(.*)$': '/lib/$1', 30 | }, 31 | 32 | // The directory where Jest should output its coverage files 33 | coverageDirectory: '/coverage', 34 | 35 | // An array of regexp pattern strings used to skip coverage collection 36 | coveragePathIgnorePatterns: [ 37 | '/node_modules/', 38 | ], 39 | 40 | // A list of reporter names that Jest uses when writing coverage reports 41 | coverageReporters: [ 42 | // "json", 43 | 'text-summary', 44 | // "text", 45 | 'lcov', 46 | // "clover" 47 | ], 48 | 49 | // An object that configures minimum threshold enforcement for coverage results 50 | // coverageThreshold: undefined, 51 | 52 | // A path to a custom dependency extractor 53 | // dependencyExtractor: undefined, 54 | 55 | // Make calling deprecated APIs throw helpful error messages 56 | // errorOnDeprecated: false, 57 | 58 | // Force coverage collection from ignored files using an array of glob patterns 59 | // forceCoverageMatch: [], 60 | 61 | // A path to a module which exports an async function that is triggered once before all test suites 62 | // globalSetup: '/test/unit/setup.js', 63 | 64 | // A path to a module which exports an async function that is triggered once after all test suites 65 | // globalTeardown: '/test/unit/teardown.js', 66 | 67 | // A set of global variables that need to be available in all test environments 68 | // globals: {}, 69 | 70 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 71 | // maxWorkers: "1", 72 | 73 | // An array of directory names to be searched recursively up from the requiring module's location 74 | // moduleDirectories: [ 75 | // "node_modules" 76 | // ], 77 | 78 | // An array of file extensions your modules use 79 | // moduleFileExtensions: [ 80 | // "js", 81 | // "json", 82 | // "jsx", 83 | // "ts", 84 | // "tsx", 85 | // "node" 86 | // ], 87 | 88 | // 模块别名设置,解析模块时要搜索的其他位置的绝对路径 89 | modulePaths: [''], 90 | 91 | // A map from regular expressions to module names that allow to stub out resources with a single module 92 | // moduleNameMapper: {}, 93 | 94 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 95 | // modulePathIgnorePatterns: [], 96 | 97 | // Activates notifications for test results 98 | // notify: false, 99 | 100 | // An enum that specifies notification mode. Requires { notify: true } 101 | // notifyMode: "failure-change", 102 | 103 | // A preset that is used as a base for Jest's configuration 104 | // preset: undefined, 105 | 106 | // Run tests from one or more projects 107 | // projects: undefined, 108 | 109 | // Use this configuration option to add custom reporters to Jest 110 | reporters: ['default'], 111 | 112 | // Automatically reset mock state between every test 113 | // resetMocks: false, 114 | 115 | // Reset the module registry before running each individual test 116 | // resetModules: false, 117 | 118 | // A path to a custom resolver 119 | // resolver: undefined, 120 | 121 | // Automatically restore mock state between every test 122 | // restoreMocks: false, 123 | 124 | // The root directory that Jest should scan for tests and modules within 125 | rootDir: './', 126 | 127 | // A list of paths to directories that Jest should use to search for files in 128 | // roots: [ 129 | // "" 130 | // ], 131 | 132 | // Allows you to use a custom runner instead of Jest's default test runner 133 | // runner: "jest-runner", 134 | 135 | // The paths to modules that run some code to configure or set up the testing environment before each test 136 | // setupFiles: [], 137 | 138 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 139 | // snapshotSerializers: [], 140 | 141 | // The test environment that will be used for testing 142 | testEnvironment: 'node', 143 | 144 | // Options that will be passed to the testEnvironment 145 | // testEnvironmentOptions: {}, 146 | 147 | // Adds a location field to test results 148 | // testLocationInResults: false, 149 | 150 | // The glob patterns Jest uses to detect test files 151 | // testMatch: [ 152 | // "**/__tests__/**/*.[jt]s?(x)", 153 | // "**/?(*.)+(spec|test).[tj]s?(x)" 154 | // ], 155 | 156 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 157 | // testPathIgnorePatterns: [ 158 | // "/node_modules/" 159 | // ], 160 | 161 | // The regexp pattern or array of patterns that Jest uses to detect test files 162 | testRegex: [ 163 | 'test/.*\\.test\\.js', 164 | ], 165 | 166 | // This option allows the use of a custom results processor 167 | // testResultsProcessor: undefined, 168 | 169 | // This option allows use of a custom test runner 170 | // testRunner: "jasmine2", 171 | 172 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 173 | // testURL: "http://localhost", 174 | 175 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 176 | // timers: "real", 177 | 178 | // A map from regular expressions to paths to transformers 179 | // transform: undefined, 180 | 181 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 182 | // transformIgnorePatterns: [ 183 | // "/node_modules/" 184 | // ], 185 | 186 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 187 | // unmockedModulePathPatterns: undefined, 188 | 189 | // Indicates whether each individual test should be reported during the run 190 | verbose: true, 191 | 192 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 193 | // watchPathIgnorePatterns: [], 194 | 195 | // Whether to use watchman for file crawling 196 | // watchman: true, 197 | }; 198 | -------------------------------------------------------------------------------- /lib/animate/alpha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * toAlphaFilter - Convert to ffmpeg alpha filter function 5 | * 6 | * ####Note: 7 | * toAlphaFilter is mainly used in the FFText component 8 | * 9 | * @function 10 | */ 11 | const Ease = require('../math/ease'); 12 | const { accAdd } = require('../math/maths'); 13 | const Utils = require('../utils/utils'); 14 | const AniFilter = require('./anifilter'); 15 | const forEach = require('lodash/forEach'); 16 | 17 | const toAlphaFilter = conf => { 18 | let { time, showType, add, ing = false, delay, ease = 'linear' } = conf; 19 | let a, elsestr; 20 | let from = showType == 'in' ? 0 : 1; 21 | let to = showType == 'in' ? 1 : 0; 22 | time = Utils.floor(time, 2); 23 | const ddelay = accAdd(delay, time); 24 | const coodi = `between(t,${delay},${ddelay})`; 25 | 26 | // Is it continuous animation or single easing animation 27 | if (ing) { 28 | if (!to) to = from + add * time; 29 | add = Utils.angleToRadian(add, 4); 30 | a = `${from}+${add}*t`; 31 | } else { 32 | a = Ease.getVal(ease, from, to - from, time, delay); 33 | } 34 | 35 | elsestr = `if(lte(t,_delay_),${to},_else_)`; 36 | let alpha = `if(${coodi}\,${a}\,_else_${to}_else_)`; 37 | alpha = Utils.replacePlusMinus(alpha); 38 | 39 | const filter = { 40 | filter: 'alpha', 41 | options: { alpha }, 42 | }; 43 | 44 | return new AniFilter({ 45 | filter, 46 | showType, 47 | name: 'alpha', 48 | type: 'object', 49 | data: { time, delay, elsestr }, 50 | }); 51 | }; 52 | 53 | /** 54 | * create new alpha filter 55 | * if(a { 59 | const elseReg = /\_else\_/gi; 60 | const delayReg = /\_delay\_/gi; 61 | const elseNelse = /\_else\_[0-9a-z]*\_else\_/gi; 62 | 63 | let a = ''; 64 | let elsea = ''; 65 | forEach(tfilters, (aniFilter, index) => { 66 | const data = aniFilter.data; 67 | const delay = data.delay; 68 | const filter = aniFilter.filter; 69 | if (index > 0) { 70 | elsea = elsea.replace(delayReg, delay).replace(elseReg, filter.options.alpha); 71 | a = a.replace(elseNelse, elsea); 72 | } else { 73 | a = String(filter.options.alpha); 74 | elsea = data.elsestr; 75 | } 76 | }); 77 | 78 | a = a.replace(elseReg, ''); 79 | const filter = { 80 | filter: 'alpha', 81 | options: { alpha: a }, 82 | }; 83 | return new AniFilter({ 84 | filter, 85 | name: 'alpha', 86 | type: 'object', 87 | }); 88 | }; 89 | 90 | /** 91 | * Replace placeholder characters in the filter field 92 | * @private 93 | */ 94 | const replaceAlphaFilter = aniFilter => { 95 | const elseReg = /\_else\_/gi; 96 | let filter = aniFilter.filter; 97 | filter.options.alpha = String(filter.options.alpha).replace(elseReg, ''); 98 | aniFilter.filter = filter; 99 | }; 100 | 101 | module.exports = { toAlphaFilter, mergeIntoNewAlphaFilter, replaceAlphaFilter }; 102 | -------------------------------------------------------------------------------- /lib/animate/anifilter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AniFilter - FFCreatorLite animation filter class 3 | * 4 | * @class 5 | */ 6 | class AniFilter { 7 | constructor({ name, filter, showType, type, data }) { 8 | this.name = name; 9 | this.filter = filter; 10 | this.type = type; 11 | this.data = data; 12 | this.showType = showType; 13 | } 14 | } 15 | 16 | module.exports = AniFilter; 17 | -------------------------------------------------------------------------------- /lib/animate/animation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFAnimation - The class used to animate the display element 5 | * 6 | * ####Example: 7 | * 8 | * const animation = new FFAnimation({ ...animation, parent }); 9 | * animation.start(); 10 | * animation.stop(); 11 | * 12 | * 13 | * ####Note: 14 | * Easeing function 15 | * Type In-Out-InOut 16 | * Ease Quadratic Cubic Quartic Quintic Exponential Circular Elastic Back Bounce 17 | * 18 | * @class 19 | */ 20 | const FFBase = require('../core/base'); 21 | const { toMoveFilter } = require('./move'); 22 | const { toFadeFilter } = require('./fade'); 23 | const { toZoomFilter } = require('./zoom'); 24 | const { toAlphaFilter } = require('./alpha'); 25 | const { toRotateFilter } = require('./rotate'); 26 | 27 | class FFAnimation extends FFBase { 28 | constructor(conf = { type: 'fade', showType: 'in', time: 2, delay: 0 }) { 29 | super({ type: 'animation', ...conf }); 30 | 31 | this.conf = conf; 32 | this.filter = null; 33 | this.isFFAni = true; 34 | } 35 | 36 | /** 37 | * Converted to ffmpeg command line parameters 38 | * @private 39 | */ 40 | toFilter() { 41 | const conf = this.getFromConf(); 42 | conf.rootConf = this.rootConf(); 43 | let method; 44 | 45 | switch (this.conf.type) { 46 | case 'move': 47 | method = toMoveFilter; 48 | break; 49 | 50 | case 'fade': 51 | case 'show': 52 | method = toFadeFilter; 53 | break; 54 | 55 | case 'rotate': 56 | method = toRotateFilter; 57 | break; 58 | 59 | case 'zoom': 60 | case 'zoompan': 61 | method = toZoomFilter; 62 | break; 63 | 64 | case 'alpha': 65 | method = toAlphaFilter; 66 | break; 67 | } 68 | 69 | if (method) this.filter = method(conf); 70 | return this.filter; 71 | } 72 | 73 | /** 74 | * Get value from conf 75 | * @private 76 | */ 77 | getFromConf(key) { 78 | return key ? this.conf[key] : this.conf; 79 | } 80 | 81 | setToConf(key, val) { 82 | this.conf[key] = val; 83 | } 84 | 85 | /** 86 | * Get from and to value from conf 87 | * @private 88 | */ 89 | getFromTo() { 90 | let { from, to, add, time, type } = this.getFromConf(); 91 | if (!to) { 92 | if (type === 'move') { 93 | to = {}; 94 | to.x = from.x + add.x * time; 95 | to.y = from.y + add.y * time; 96 | } else if (type === 'zoom') { 97 | const fps = this.rootConf().getVal('fps'); 98 | const frames = fps * time; 99 | to = from + add * frames; 100 | } else { 101 | to = from + add * time; 102 | } 103 | } 104 | 105 | return { from, to }; 106 | } 107 | } 108 | 109 | module.exports = FFAnimation; 110 | -------------------------------------------------------------------------------- /lib/animate/animations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFAnimations - A collection class used to manage animation 5 | * 6 | * ####Example: 7 | * 8 | * const animations = new FFAnimations(this); 9 | * animations.setAnimations(animations); 10 | * animations.addAnimate(animation); 11 | * 12 | * 13 | * @class 14 | */ 15 | const forEach = require('lodash/forEach'); 16 | const cloneDeep = require('lodash/cloneDeep'); 17 | const Utils = require('../utils/utils'); 18 | const Effects = require('./effects'); 19 | const FFAnimation = require('./animation'); 20 | const { mergeIntoNewAlphaFilter, replaceAlphaFilter } = require('./alpha'); 21 | const { mergeIntoNewZoompanFilter, replaceZoomFilter } = require('./zoom'); 22 | const { mergeIntoNewRotateFilter, replaceRotateFilter } = require('./rotate'); 23 | const { mergeIntoNewOverflyFilter, replaceOverflyFilter } = require('./move'); 24 | 25 | class FFAnimations { 26 | constructor(animations) { 27 | this.list = []; 28 | this.setAnimations(animations); 29 | } 30 | 31 | /** 32 | * Set target object 33 | * @param {FFNode} target - the target object 34 | * @public 35 | */ 36 | setTarget(target) { 37 | this.target = target; 38 | } 39 | 40 | /** 41 | * Set animations array 42 | * @param {Array} animations - animations array 43 | * @public 44 | */ 45 | setAnimations(animations = []) { 46 | forEach(animations, ani => this.addAnimate(ani)); 47 | } 48 | 49 | /** 50 | * add default effect animation 51 | * @param {String} type - effect type 52 | * @param {Number} time - effect time 53 | * @param {Number} delay - effect delay 54 | * @public 55 | */ 56 | addEffect(type, time, delay) { 57 | let conf = {}; 58 | if (time === undefined) { 59 | conf = Utils.clone(type); 60 | type = conf.type; 61 | time = conf.time; 62 | delay = conf.delay; 63 | } else { 64 | conf = { type, time, delay }; 65 | } 66 | 67 | const params = cloneDeep(Effects.effects[type]); 68 | if (!params) return; 69 | 70 | if (Array.isArray(params)) { 71 | forEach(params, p => { 72 | Utils.excludeBind(p, conf, ['type']); 73 | this.addAnimate(p); 74 | }); 75 | } else { 76 | Utils.excludeBind(params, conf, ['type']); 77 | this.addAnimate(params); 78 | } 79 | } 80 | 81 | addAnimate(animation) { 82 | this.replaceFadeToAlphaByText(animation); 83 | 84 | if (!animation.isFFAni) animation = new FFAnimation({ ...animation }); 85 | this.list.push(animation); 86 | animation.parent = this.target; 87 | return animation; 88 | } 89 | 90 | replaceFadeToAlphaByText(animation) { 91 | if (this.target.type != 'text') return animation; 92 | if (!animation.isFFAni) { 93 | if (animation.type == 'fade' || animation.type == 'show') animation.type = 'alpha'; 94 | } else { 95 | if (animation.getFromConf('type') == 'fade' || animation.getFromConf('type') == 'show') 96 | animation.setToConf('type', 'alpha'); 97 | } 98 | 99 | return animation; 100 | } 101 | 102 | hasAnimate(itype) { 103 | let has = false; 104 | forEach(this.list, ani => { 105 | const type = ani.getFromConf('type'); 106 | if (type === itype) has = true; 107 | }); 108 | 109 | return has; 110 | } 111 | 112 | hasZoompanPad() { 113 | let has = false; 114 | forEach(this.list, ani => { 115 | const type = ani.getFromConf('type'); 116 | const pad = ani.getFromConf('pad'); 117 | if (type == 'zoom' && pad) has = true; 118 | }); 119 | 120 | return has; 121 | } 122 | 123 | /** 124 | * get max scale value from scale conf 125 | * @private 126 | */ 127 | getMaxScale() { 128 | let maxScale = 1; 129 | forEach(this.list, ani => { 130 | const pad = ani.getFromConf('pad'); 131 | const type = ani.getFromConf('type'); 132 | if (type == 'zoom' && pad) { 133 | const { from, to } = ani.getFromTo(); 134 | maxScale = Math.max(from, to); 135 | } 136 | }); 137 | return maxScale; 138 | } 139 | 140 | /** 141 | * concat all animation filters 142 | * @private 143 | */ 144 | concatFilters() { 145 | let filters = this.mergeListFilters(); 146 | // Filter order is very important 147 | // rotate -> zoompan -> fade -> overlay 148 | filters = this.mergeSpecialFilters('alpha', filters); 149 | filters = this.mergeSpecialFilters('rotate', filters); 150 | filters = this.mergeSpecialFilters('zoompan', filters); 151 | filters = this.swapFadeFilterPosition(filters); 152 | filters = this.mergeSpecialFilters('overlay', filters); 153 | filters = this.convertToFFmpegFilter(filters); 154 | 155 | return filters; 156 | } 157 | 158 | convertToFFmpegFilter(filters) { 159 | let cfilters = []; 160 | forEach(filters, filter => cfilters.push(filter.filter)); 161 | return cfilters; 162 | } 163 | 164 | replaceEffectConfVal() { 165 | forEach(this.list, ani => { 166 | const { conf } = ani; 167 | for (let key in conf) { 168 | const val = conf[key]; 169 | const newVal = Effects.mapping(val, this.target); 170 | conf[key] = newVal === null ? val : newVal; 171 | } 172 | }); 173 | } 174 | 175 | swapFadeFilterPosition(filters) { 176 | let cfilters = [...filters]; 177 | let tfilters = []; 178 | 179 | // 1. filter and delete all overlay 180 | forEach(filters, aniFilter => { 181 | if (aniFilter.name === 'fade') { 182 | tfilters.push(aniFilter); 183 | Utils.deleteArrayElement(cfilters, aniFilter); 184 | } 185 | }); 186 | 187 | return cfilters.concat(tfilters); 188 | } 189 | 190 | mergeListFilters() { 191 | let filters = []; 192 | forEach(this.list, ani => { 193 | const filter = ani.toFilter(); 194 | if (filter.type == 'array') { 195 | filters = filters.concat(filter); 196 | } else { 197 | filters.push(filter); 198 | } 199 | }); 200 | 201 | return filters; 202 | } 203 | 204 | /** 205 | * merge any specia filters 206 | * @private 207 | */ 208 | mergeSpecialFilters(type, filters) { 209 | let cfilters = [...filters]; 210 | let tfilters = []; 211 | 212 | // 1. filter and delete all overlay 213 | forEach(filters, aniFilter => { 214 | if (aniFilter.name === type) { 215 | tfilters.push(aniFilter); 216 | Utils.deleteArrayElement(cfilters, aniFilter); 217 | } 218 | }); 219 | 220 | // 2-1. if only one push all overlay to last 221 | // [filter] -> [cfilters ...filter(last)] 222 | if (tfilters.length == 1) { 223 | const aniFilter = tfilters[0]; 224 | if (type == 'zoompan') { 225 | replaceZoomFilter(aniFilter); 226 | } else if (type == 'overlay') { 227 | replaceOverflyFilter(aniFilter); 228 | } else if (type == 'rotate') { 229 | replaceRotateFilter(aniFilter); 230 | } else if (type == 'alpha') { 231 | replaceAlphaFilter(aniFilter); 232 | } 233 | 234 | cfilters = cfilters.concat(tfilters); 235 | } 236 | 237 | // 2-2 if more than one merge all tfilters 238 | // 1. [filter-in, filter-out] -> [cfilters ...filter-in, filter-out(last)] 239 | else if (tfilters.length > 1) { 240 | tfilters = Utils.sortArrayByKey(tfilters, 'showType', 'in'); 241 | let newFilter; 242 | if (type == 'zoompan') { 243 | newFilter = mergeIntoNewZoompanFilter(tfilters); 244 | } else if (type == 'overlay') { 245 | newFilter = mergeIntoNewOverflyFilter(tfilters); 246 | } else if (type == 'rotate') { 247 | newFilter = mergeIntoNewRotateFilter(tfilters); 248 | } else if (type == 'alpha') { 249 | newFilter = mergeIntoNewAlphaFilter(tfilters); 250 | } 251 | 252 | newFilter && cfilters.push(newFilter); 253 | } 254 | 255 | return cfilters; 256 | } 257 | 258 | /** 259 | * get duration from animations 260 | * @private 261 | */ 262 | getDuration() { 263 | let duration = 0; 264 | forEach(this.list, ani => { 265 | const name = ani.getFromConf('name'); 266 | const time = ani.getFromConf('time'); 267 | const delay = ani.getFromConf('delay'); 268 | const showType = ani.getFromConf('showType'); 269 | 270 | if (showType === 'out' || name === 'no') { 271 | duration = time + delay; 272 | } 273 | }); 274 | 275 | return duration; 276 | } 277 | 278 | /** 279 | * modify delay time Less appearTime 280 | * @private 281 | */ 282 | modifyDelayTime(appearTime) { 283 | forEach(this.list, ani => { 284 | let delay = ani.getFromConf('delay'); 285 | delay -= appearTime; 286 | ani.setToConf('delay', Math.max(delay, 0)); 287 | }); 288 | } 289 | 290 | /** 291 | * Get appearTime from animations 292 | * @private 293 | */ 294 | getAppearTime() { 295 | let appearTime = 0; 296 | forEach(this.list, ani => { 297 | const showType = ani.getFromConf('showType'); 298 | const delay = ani.getFromConf('delay'); 299 | if (showType === 'in') { 300 | appearTime = delay || 0; 301 | } 302 | }); 303 | return appearTime; 304 | } 305 | } 306 | 307 | module.exports = FFAnimations; 308 | -------------------------------------------------------------------------------- /lib/animate/effects.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Effects - Some simulation css animation effects collection 5 | * Effect realizes the animation of `animate.css` _4.1.0_ version https://animate.style/ 6 | * 7 | * ####Example: 8 | * 9 | * const ani = Effects.getAnimation({ type, time, delay }, attr); 10 | * 11 | * 12 | * 13 | * @class 14 | */ 15 | 16 | const Effects = {}; 17 | 18 | // mapping key 19 | const target = '_target_'; 20 | const targetLeft = '_target_left_'; 21 | const targetRight = '_target_right_'; 22 | const targetUp = '_target_up_'; 23 | const targetBottom = '_target_bottom_'; 24 | const targetLeftBig = '_target_left_big_'; 25 | const targetRightBig = '_target_right_big_'; 26 | const targetUpBig = '_target_up_big_'; 27 | const targetBottomBig = '_target_bottom_big_'; 28 | const targetRotate = '_target_rotate_'; 29 | const targetRotateAdd = '_target_rotate_add_'; 30 | const targetRotateAddBig = '_target_rotate_add_big_'; 31 | const zoomBig = '_zoom_big_'; 32 | const zoomBiging = '_zoom_biging_'; 33 | const targetSize = '_target_size_'; 34 | 35 | // base props 36 | const time = 3; 37 | const inDelay = 0; 38 | const outDelay = 5; 39 | const zoomingSpeed = 0.005; 40 | const moveingSpeed = 50; 41 | const ins = { showType: 'in', time, delay: inDelay }; 42 | const outs = { showType: 'out', time, delay: outDelay }; 43 | const ingins = { showType: 'in', time: 60 * 60, delay: inDelay, ing: true }; 44 | const ingouts = { showType: 'out', time: 60 * 60, delay: outDelay, ing: true }; 45 | 46 | // all effects 47 | Effects.effects = { 48 | // no 49 | no: { type: 'move', ...ins, from: target, to: target, name: 'no' }, 50 | show: { type: 'move', ...ins, from: target, to: target, name: 'no' }, 51 | 52 | // fade 53 | fadeIn: { type: 'show', ...ins }, 54 | fadeOut: { type: 'show', ...outs }, 55 | 56 | // move normal 57 | moveInLeft: [ 58 | { type: 'show', ...ins }, 59 | { type: 'move', ...ins, from: targetLeft, to: target, ease: 'quadOut' }, 60 | ], 61 | moveOutLeft: [ 62 | { type: 'show', ...outs }, 63 | { type: 'move', ...outs, from: target, to: targetLeft, ease: 'quadIn' }, 64 | ], 65 | moveInRight: [ 66 | { type: 'show', ...ins }, 67 | { type: 'move', ...ins, from: targetRight, to: target, ease: 'quadOut' }, 68 | ], 69 | moveOutRight: [ 70 | { type: 'show', ...outs }, 71 | { type: 'move', ...outs, from: target, to: targetRight, ease: 'quadIn' }, 72 | ], 73 | moveInUp: [ 74 | { type: 'show', ...ins }, 75 | { type: 'move', ...ins, from: targetUp, to: target, ease: 'quadOut' }, 76 | ], 77 | moveOutUp: [ 78 | { type: 'show', ...outs }, 79 | { type: 'move', ...outs, from: target, to: targetUp, ease: 'quadIn' }, 80 | ], 81 | moveInBottom: [ 82 | { type: 'show', ...ins }, 83 | { type: 'move', ...ins, from: targetBottom, to: target, ease: 'quadOut' }, 84 | ], 85 | moveOutBottom: [ 86 | { type: 'show', ...outs }, 87 | { type: 'move', ...outs, from: target, to: targetBottom, ease: 'quadIn' }, 88 | ], 89 | 90 | // move big 91 | moveInLeftBig: [ 92 | { type: 'show', ...ins }, 93 | { type: 'move', ...ins, from: targetLeftBig, to: target, ease: 'quadOut' }, 94 | ], 95 | moveOutLeftBig: [ 96 | { type: 'show', ...outs }, 97 | { type: 'move', ...outs, from: target, to: targetLeftBig, ease: 'quadIn' }, 98 | ], 99 | moveInRightBig: [ 100 | { type: 'show', ...ins }, 101 | { type: 'move', ...ins, from: targetRightBig, to: target, ease: 'quadOut' }, 102 | ], 103 | moveOutRightBig: [ 104 | { type: 'show', ...outs }, 105 | { type: 'move', ...outs, from: target, to: targetRightBig, ease: 'quadIn' }, 106 | ], 107 | moveInUpBig: [ 108 | { type: 'show', ...ins }, 109 | { type: 'move', ...ins, from: targetUpBig, to: target, ease: 'quadOut' }, 110 | ], 111 | moveOutUpBig: [ 112 | { type: 'show', ...outs }, 113 | { type: 'move', ...outs, from: target, to: targetUpBig, ease: 'quadIn' }, 114 | ], 115 | moveInBottomBig: [ 116 | { type: 'show', ...ins }, 117 | { type: 'move', ...ins, from: targetBottomBig, to: target, ease: 'quadOut' }, 118 | ], 119 | moveOutBottomBig: [ 120 | { type: 'show', ...outs }, 121 | { type: 'move', ...outs, from: target, to: targetBottomBig, ease: 'quadIn' }, 122 | ], 123 | 124 | // move ease back 125 | moveInLeftBack: [ 126 | { type: 'show', ...ins }, 127 | { type: 'move', ...ins, from: targetLeftBig, to: target, ease: 'backOut' }, 128 | ], 129 | moveOutLeftBack: [ 130 | { type: 'show', ...outs }, 131 | { type: 'move', ...outs, from: target, to: targetLeftBig, ease: 'backIn' }, 132 | ], 133 | moveInRightBack: [ 134 | { type: 'show', ...ins }, 135 | { type: 'move', ...ins, from: targetRightBig, to: target, ease: 'backOut' }, 136 | ], 137 | moveOutRightBack: [ 138 | { type: 'show', ...outs }, 139 | { type: 'move', ...outs, from: target, to: targetRightBig, ease: 'backIn' }, 140 | ], 141 | moveInUpBack: [ 142 | { type: 'show', ...ins }, 143 | { type: 'move', ...ins, from: targetUpBig, to: target, ease: 'backOut' }, 144 | ], 145 | moveOutUpBack: [ 146 | { type: 'show', ...outs }, 147 | { type: 'move', ...outs, from: target, to: targetUpBig, ease: 'backIn' }, 148 | ], 149 | moveInBottomBack: [ 150 | { type: 'show', ...ins }, 151 | { type: 'move', ...ins, from: targetBottomBig, to: target, ease: 'backOut' }, 152 | ], 153 | moveOutBottomBack: [ 154 | { type: 'show', ...outs }, 155 | { type: 'move', ...outs, from: target, to: targetBottomBig, ease: 'backIn' }, 156 | ], 157 | 158 | // rotate in out 159 | rotateIn: [ 160 | { type: 'show', ...ins }, 161 | { type: 'rotate', ...ins, from: targetRotateAdd, to: targetRotate, ease: 'linear' }, 162 | ], 163 | rotateOut: [ 164 | { type: 'show', ...outs }, 165 | { type: 'rotate', ...outs, from: targetRotate, to: targetRotateAdd, ease: 'quadIn' }, 166 | ], 167 | rotateInBig: [ 168 | { type: 'show', ...ins }, 169 | { type: 'rotate', ...ins, from: targetRotateAddBig, to: targetRotate, ease: 'quadOut' }, 170 | ], 171 | rotateOutBig: [ 172 | { type: 'show', ...outs }, 173 | { type: 'rotate', ...outs, from: targetRotate, to: targetRotateAddBig, ease: 'quadIn' }, 174 | ], 175 | 176 | // zoom in out 177 | zoomIn: [ 178 | { type: 'zoom', ...ins, from: 1 / 2, to: 1, size: targetSize, pad: true }, 179 | { type: 'show', ...ins }, 180 | ], 181 | zoomOut: [ 182 | { type: 'zoom', ...outs, from: 2, to: 1, size: targetSize, pad: true }, 183 | { type: 'show', ...outs }, 184 | ], 185 | zoomNopadIn: [ 186 | { type: 'zoom', ...ins, from: 1.5, to: 1, size: targetSize }, 187 | { type: 'show', ...ins }, 188 | ], 189 | zoomNopadOut: [ 190 | { type: 'zoom', ...outs, from: 1, to: 1.5, size: targetSize }, 191 | { type: 'show', ...outs }, 192 | ], 193 | zoomInUp: [ 194 | { type: 'zoom', ...ins, from: 1, to: 2, size: targetSize, pad: true }, 195 | { type: 'move', ...ins, from: targetUp, to: target, ease: 'quadOut' }, 196 | { type: 'show', ...ins }, 197 | ], 198 | zoomOutUp: [ 199 | { type: 'zoom', ...outs, from: 2, to: 1, size: targetSize, pad: true }, 200 | { type: 'move', ...outs, from: target, to: targetUp }, 201 | { type: 'show', ...outs }, 202 | ], 203 | zoomInDown: [ 204 | { type: 'zoom', ...ins, from: 1, to: 2, size: targetSize, pad: true }, 205 | { type: 'move', ...ins, from: targetBottom, to: target, ease: 'quadOut' }, 206 | { type: 'show', ...ins }, 207 | ], 208 | zoomOutDown: [ 209 | { type: 'zoom', ...outs, from: 2, to: 1, size: targetSize, pad: true }, 210 | { type: 'move', ...outs, from: target, to: targetBottom }, 211 | { type: 'show', ...outs }, 212 | ], 213 | 214 | // background effect ing,,, 215 | zoomingIn: [{ type: 'zoom', ...ingins, from: 1, add: zoomingSpeed, size: targetSize }], 216 | zoomingOut: [{ type: 'zoom', ...ingins, from: 2, add: -zoomingSpeed, size: targetSize }], 217 | moveingLeft: [{ type: 'move', ...ingins, from: target, add: { x: -moveingSpeed, y: 0 } }], 218 | moveingRight: [{ type: 'move', ...ingins, from: target, add: { x: moveingSpeed, y: 0 } }], 219 | moveingUp: [{ type: 'move', ...ingins, from: target, add: { x: 0, y: -moveingSpeed } }], 220 | moveingBottom: [{ type: 'move', ...ingins, from: target, add: { x: 0, y: moveingSpeed } }], 221 | fadingIn: { type: 'show', ...ingins }, 222 | fadingOut: { type: 'show', ...ingouts }, 223 | }; 224 | 225 | // Map pronouns to numeric values 226 | Effects.mapping = (key, obj) => { 227 | let val = null; 228 | const minDis = 100; 229 | const maxDis = 400; 230 | 231 | switch (key) { 232 | case target: 233 | val = { x: obj.x, y: obj.y }; 234 | break; 235 | 236 | // up / down/ left/ right 237 | case targetLeft: 238 | val = { x: obj.x - minDis, y: obj.y }; 239 | break; 240 | case targetRight: 241 | val = { x: obj.x + minDis, y: obj.y }; 242 | break; 243 | case targetUp: 244 | val = { x: obj.x, y: obj.y - minDis }; 245 | break; 246 | case targetBottom: 247 | val = { x: obj.x, y: obj.y + minDis }; 248 | break; 249 | 250 | // big up / down/ left/ right 251 | case targetLeftBig: 252 | val = { x: obj.x - maxDis, y: obj.y }; 253 | break; 254 | case targetRightBig: 255 | val = { x: obj.x + maxDis, y: obj.y }; 256 | break; 257 | case targetUpBig: 258 | val = { x: obj.x, y: obj.y - maxDis }; 259 | break; 260 | case targetBottomBig: 261 | val = { x: obj.x, y: obj.y + maxDis }; 262 | break; 263 | 264 | // rotate 265 | case targetRotate: 266 | val = obj.rotate; 267 | break; 268 | case targetRotateAdd: 269 | val = obj.rotate + 60; 270 | break; 271 | case targetRotateAddBig: 272 | val = obj.rotate + 180; 273 | break; 274 | 275 | // zoom 276 | case zoomBig: 277 | val = 1.5; 278 | break; 279 | case zoomBiging: 280 | val = 2; 281 | break; 282 | case targetSize: 283 | val = obj.getSize(); 284 | break; 285 | } 286 | 287 | return val; 288 | }; 289 | 290 | module.exports = Effects; 291 | -------------------------------------------------------------------------------- /lib/animate/fade.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * toFadeFilter - Convert to ffmpeg fade filter function 5 | * 6 | * @function 7 | */ 8 | const Utils = require('../utils/utils'); 9 | const AniFilter = require('./anifilter'); 10 | 11 | const toFadeFilter = conf => { 12 | let { time, delay, showType = 'in', alpha = 1, color = 'black' } = conf; 13 | 14 | time = Utils.floor(time, 2); 15 | delay = Utils.floor(delay, 2); 16 | 17 | const filter = { 18 | filter: 'fade', 19 | options: { 20 | type: showType, 21 | st: delay, 22 | d: time, 23 | color, 24 | alpha, 25 | }, 26 | }; 27 | 28 | return new AniFilter({ 29 | filter, 30 | showType, 31 | name: 'fade', 32 | type: 'object', 33 | data: { time, delay }, 34 | }); 35 | }; 36 | 37 | module.exports = { toFadeFilter }; 38 | -------------------------------------------------------------------------------- /lib/animate/move.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * toMoveFilter - Convert to ffmpeg overlay filter function 5 | * 6 | * @function 7 | */ 8 | const Ease = require('../math/ease'); 9 | const { accAdd } = require('../math/maths'); 10 | const Utils = require('../utils/utils'); 11 | const AniFilter = require('./anifilter'); 12 | const forEach = require('lodash/forEach'); 13 | 14 | const toMoveFilter = conf => { 15 | let x, y, movex, movey, elsestr; 16 | let { from, to, time, delay, showType, add, ing = false, ease = 'linear' } = conf; 17 | time = Utils.floor(time, 2); 18 | const ddelay = accAdd(delay, time); 19 | const coodi = `between(t,${delay},${ddelay})`; 20 | 21 | // Is it continuous animation or single easing animation 22 | if (ing) { 23 | if (!to) { 24 | to = {}; 25 | to.x = from.x + add.x * time; 26 | to.y = from.y + add.y * time; 27 | } 28 | movex = `${from.x}+${add.x}*t`; 29 | movey = `${from.y}+${add.y}*t`; 30 | } else { 31 | movex = Ease.getVal(ease, from.x, to.x - from.x, time, delay); 32 | movey = Ease.getVal(ease, from.y, to.y - from.y, time, delay); 33 | } 34 | 35 | elsestr = {}; 36 | elsestr.x = `if(lte(t,_delay_),${to.x},_else_)`; 37 | elsestr.y = `if(lte(t,_delay_),${to.y},_else_)`; 38 | 39 | x = `if(${coodi}\,${movex}\,_else_${to.x}_else_)`; 40 | y = `if(${coodi}\,${movey}\,_else_${to.y}_else_)`; 41 | x = Utils.replacePlusMinus(x); 42 | y = Utils.replacePlusMinus(y); 43 | const filter = { filter: 'overlay', options: { x, y } }; 44 | 45 | return new AniFilter({ 46 | filter, 47 | showType, 48 | name: 'overlay', 49 | type: 'object', 50 | data: { time, delay, elsestr }, 51 | }); 52 | }; 53 | 54 | /** 55 | * create new overlay filter 56 | * if(a { 60 | const elseReg = /\_else\_/gi; 61 | const delayReg = /\_delay\_/gi; 62 | const elseNelse = /\_else\_[0-9a-z]*\_else\_/gi; 63 | 64 | let x = ''; 65 | let y = ''; 66 | let elsex, elsey; 67 | 68 | // if(lte(t,_delay_),${to.x},_else_) 69 | forEach(tfilters, (aniFilter, index) => { 70 | const data = aniFilter.data; 71 | const delay = data.delay; 72 | const filter = aniFilter.filter; 73 | if (index > 0) { 74 | elsex = elsex.replace(delayReg, delay).replace(elseReg, filter.options.x); 75 | elsey = elsey.replace(delayReg, delay).replace(elseReg, filter.options.y); 76 | x = x.replace(elseNelse, elsex); 77 | y = y.replace(elseNelse, elsey); 78 | } else { 79 | x = String(filter.options.x); 80 | y = String(filter.options.y); 81 | elsex = data.elsestr.x; 82 | elsey = data.elsestr.y; 83 | } 84 | }); 85 | 86 | x = x.replace(elseReg, ''); 87 | y = y.replace(elseReg, ''); 88 | 89 | return new AniFilter({ 90 | filter: `overlay=x='${x}':y='${y}'`, 91 | name: 'overlay', 92 | type: 'string', 93 | }); 94 | }; 95 | 96 | /** 97 | * Replace placeholder characters in the filter field 98 | * @private 99 | */ 100 | const replaceOverflyFilter = aniFilter => { 101 | const elseReg = /\_else\_/gi; 102 | let filter = aniFilter.filter; 103 | filter.options.x = String(filter.options.x).replace(elseReg, ''); 104 | filter.options.y = String(filter.options.y).replace(elseReg, ''); 105 | aniFilter.filter = filter; 106 | }; 107 | 108 | module.exports = { toMoveFilter, mergeIntoNewOverflyFilter, replaceOverflyFilter }; 109 | -------------------------------------------------------------------------------- /lib/animate/rotate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * toRotateFilter - Convert to ffmpeg rotate filter function 5 | * 6 | * @function 7 | */ 8 | const Ease = require('../math/ease'); 9 | const { accAdd } = require('../math/maths'); 10 | const Utils = require('../utils/utils'); 11 | const AniFilter = require('./anifilter'); 12 | const forEach = require('lodash/forEach'); 13 | 14 | const toRotateFilter = conf => { 15 | let rotate, elsestr; 16 | let { from, to, time, showType, add, ing = false, delay, ease = 'linear' } = conf; 17 | time = Utils.floor(time, 2); 18 | from = Utils.angleToRadian(from, 4); 19 | to = Utils.angleToRadian(to, 4); 20 | 21 | const ddelay = accAdd(delay, time); 22 | const coodi = `between(t,${delay},${ddelay})`; 23 | 24 | // Is it continuous animation or single easing animation 25 | if (ing) { 26 | if (!to) to = from + add * time; 27 | add = Utils.angleToRadian(add, 4); 28 | rotate = `${from}+${add}*t`; 29 | } else { 30 | rotate = Ease.getVal(ease, from, to - from, time, delay); 31 | } 32 | 33 | elsestr = `if(lte(t,_delay_),${to},_else_)`; 34 | let a = `if(${coodi}\,${rotate}\,_else_${to}_else_)`; 35 | a = Utils.replacePlusMinus(a); 36 | 37 | const filter = { 38 | filter: 'rotate', 39 | options: { a, ow: 'hypot(iw,ih)', oh: 'ow', c: `black@0` }, 40 | }; 41 | 42 | return new AniFilter({ 43 | filter, 44 | showType, 45 | name: 'rotate', 46 | type: 'object', 47 | data: { time, delay, elsestr }, 48 | }); 49 | }; 50 | 51 | /** 52 | * create new rotate filter 53 | * if(a { 57 | const elseReg = /\_else\_/gi; 58 | const delayReg = /\_delay\_/gi; 59 | const elseNelse = /\_else\_[0-9a-z]*\_else\_/gi; 60 | 61 | let a = ''; 62 | let elsea = ''; 63 | forEach(tfilters, (aniFilter, index) => { 64 | const data = aniFilter.data; 65 | const delay = data.delay; 66 | const filter = aniFilter.filter; 67 | if (index > 0) { 68 | elsea = elsea.replace(delayReg, delay).replace(elseReg, filter.options.a); 69 | a = a.replace(elseNelse, elsea); 70 | } else { 71 | a = String(filter.options.a); 72 | elsea = data.elsestr; 73 | } 74 | }); 75 | 76 | a = a.replace(elseReg, ''); 77 | const filter = { 78 | filter: 'rotate', 79 | options: { a, ow: 'hypot(iw,ih)', oh: 'ow', c: `black@0` }, 80 | }; 81 | return new AniFilter({ 82 | filter, 83 | name: 'rotate', 84 | type: 'object', 85 | }); 86 | }; 87 | 88 | /** 89 | * Replace placeholder characters in the filter field 90 | * @private 91 | */ 92 | const replaceRotateFilter = aniFilter => { 93 | const elseReg = /\_else\_/gi; 94 | let filter = aniFilter.filter; 95 | filter.options.a = String(filter.options.a).replace(elseReg, ''); 96 | aniFilter.filter = filter; 97 | }; 98 | 99 | module.exports = { toRotateFilter, mergeIntoNewRotateFilter, replaceRotateFilter }; 100 | -------------------------------------------------------------------------------- /lib/animate/transition.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFTransition - Class used to handle scene transition animation 5 | * 6 | * ####Example: 7 | * 8 | * const transition = new FFTransition({ name, duration, params }); 9 | * // https://trac.ffmpeg.org/wiki/Xfade 10 | * 11 | * @object 12 | */ 13 | const FFBase = require('../core/base'); 14 | const DateUtil = require('../utils/date'); 15 | 16 | class FFTransition extends FFBase { 17 | constructor(conf) { 18 | super({ type: 'transition', ...conf }); 19 | 20 | const { name = 'fade', duration = 600 } = this.conf; 21 | this.name = name; 22 | this.offset = 0; 23 | this.duration = DateUtil.toSeconds(duration); 24 | } 25 | 26 | /** 27 | * Converted to ffmpeg command line parameters 28 | * @private 29 | */ 30 | toFilter(aoffset) { 31 | const { offset, duration, name } = this; 32 | return { 33 | filter: 'xfade', 34 | options: { 35 | transition: name, 36 | duration, 37 | offset: offset + aoffset, 38 | }, 39 | }; 40 | } 41 | 42 | destroy() {} 43 | } 44 | 45 | module.exports = FFTransition; 46 | -------------------------------------------------------------------------------- /lib/animate/zoom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * toZoomFilter - Convert to zoom filter function 5 | * 6 | * ####Note: 7 | * Fix ffmpeg zoompan jiggle bug. 8 | * https://superuser.com/questions/1112617/ffmpeg-smooth-zoompan-with-no-jiggle/1112680#1112680 9 | * 10 | * 11 | * @function 12 | */ 13 | const forEach = require('lodash/forEach'); 14 | const Ease = require('../math/ease'); 15 | const Utils = require('../utils/utils'); 16 | const AniFilter = require('./anifilter'); 17 | const FilterUtil = require('../utils/filter'); 18 | 19 | const toZoomFilter = conf => { 20 | let { from = 1, to, showType, time, delay, add, ing = false, size, pad, ease = 'linear' } = conf; 21 | let zoom, elsestr, filter; 22 | let maxScale = 1; 23 | let x = 'iw/2-(iw/zoom/2)'; 24 | let y = 'ih/2-(ih/zoom/2)'; 25 | 26 | // reset from to value 27 | if (from < 1 && pad) { 28 | to = (1 / from) * to; 29 | from = 1; 30 | } 31 | 32 | const fps = conf.rootConf.getVal('fps'); 33 | const frames = fps * time; 34 | const delayFrames = fps * delay; 35 | // (max>=x>=min) 36 | const coodi = `between(on,${delayFrames - 1},${delayFrames + frames})`; 37 | 38 | // Is it continuous animation or single easing animation 39 | if (ing) { 40 | if (!to) to = from + add * frames; 41 | zoom = `${from}+${add}*(on-${delayFrames})`; 42 | } else { 43 | zoom = Ease.getVal(ease, from, to - from, frames, delayFrames).replace(/t/gi, 'on'); 44 | } 45 | 46 | //- 47 | elsestr = `if(lte(on,_delay_),${to},_else_)`; 48 | let z = `if(${coodi}\,${zoom}\,_else_${to}_else_)`; 49 | z = Utils.replacePlusMinus(z); 50 | 51 | //- 52 | maxScale = Math.max(from, to); 53 | const padFilter = FilterUtil.createPadFilter(maxScale); 54 | const scaleFilter = FilterUtil.createScaleFilter(4000); 55 | const zoomFilter = FilterUtil.createZoomFilter({ x, y, z, s: size, fps, d: frames }); 56 | filter = pad ? `${padFilter},${scaleFilter},${zoomFilter}` : `${scaleFilter},${zoomFilter}`; 57 | 58 | return new AniFilter({ 59 | filter, 60 | showType, 61 | name: 'zoompan', 62 | type: 'string', 63 | data: { time, delay: delayFrames, elsestr, z }, 64 | }); 65 | }; 66 | 67 | /** 68 | * create new zoompan filter 69 | * if(a { 73 | const elseReg = /\_else\_/gi; 74 | const delayReg = /\_delay\_/gi; 75 | const elseNelse = /\_else\_[0-9a-z]*\_else\_/gi; 76 | 77 | let zoom = ''; 78 | let elsez; 79 | 80 | // if(lte(t,_delay_),${to.x},_else_) 81 | forEach(tfilters, (aniFilter, index) => { 82 | const data = aniFilter.data; 83 | const delay = data.delay; 84 | const z = data.z; 85 | const filter = aniFilter.filter; 86 | if (index > 0) { 87 | elsez = elsez.replace(delayReg, delay).replace(elseReg, z); 88 | zoom = zoom.replace(elseNelse, elsez); 89 | } else { 90 | zoom = String(filter); 91 | elsez = data.elsestr; 92 | } 93 | }); 94 | 95 | zoom = zoom.replace(elseReg, ''); 96 | return new AniFilter({ 97 | filter: zoom, 98 | name: 'zoompan', 99 | type: 'string', 100 | }); 101 | }; 102 | 103 | /** 104 | * Replace placeholder characters in the filter field 105 | * @private 106 | */ 107 | const replaceZoomFilter = aniFilter => { 108 | const elseReg = /\_else\_/gi; 109 | let filter = aniFilter.filter; 110 | filter = String(filter).replace(elseReg, ''); 111 | aniFilter.filter = filter; 112 | }; 113 | 114 | module.exports = { toZoomFilter, mergeIntoNewZoompanFilter, replaceZoomFilter }; 115 | -------------------------------------------------------------------------------- /lib/assets/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drawcall/FFCreatorLite/3fab3a188d392052763c7f1987340180fb626d49/lib/assets/blank.png -------------------------------------------------------------------------------- /lib/conf/conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Conf - A encapsulated configuration class is used to better manage configuration related 5 | * 6 | * ####Example: 7 | * 8 | * const conf = new Conf(conf); 9 | * const val = conf.getVal(key); 10 | * conf.setVal(key, val); 11 | * 12 | * @object 13 | */ 14 | const path = require('path'); 15 | const tempy = require('tempy'); 16 | const Utils = require('../utils/utils'); 17 | 18 | class Conf { 19 | constructor(conf = {}) { 20 | this.conf = conf; 21 | 22 | this.conf.pathId = Utils.uid(); 23 | this.copyByDefaultVal(conf, 'crf', 20); 24 | this.copyByDefaultVal(conf, 'vb', null); 25 | this.copyByDefaultVal(conf, 'queue', 2); 26 | this.copyByDefaultVal(conf, 'threads', 1); 27 | this.copyByDefaultVal(conf, 'preset', 'medium'); 28 | this.copyByDefaultVal(conf, 'vprofile', 'main'); 29 | this.copyByDefaultVal(conf, 'debug', false); 30 | this.copyByDefaultVal(conf, 'logFFmpegError', false); 31 | this.copyByDefaultVal(conf, 'audioLoop', true); 32 | this.copyByDefaultVal(conf, 'upStreaming', false); 33 | this.copyByDefaultVal(conf, 'cacheFormat', 'mp4'); 34 | this.copyByDefaultVal(conf, 'hasTransition', false); 35 | this.copyByDefaultVal(conf, 'defaultOutputOptions', true); 36 | this.copyFromMultipleVal(conf, 'fps', 'fps', 24); 37 | this.copyFromMultipleVal(conf, 'rfps', 'rfps', 24); 38 | this.copyFromMultipleVal(conf, 'width', 'w', 800); 39 | this.copyFromMultipleVal(conf, 'height', 'h', 450); 40 | this.copyFromMultipleVal(conf, 'outputDir', 'dir', path.join('./')); 41 | this.copyFromMultipleVal(conf, 'cacheDir', 'temp', tempy.directory()); 42 | this.copyFromMultipleVal(conf, 'output', 'out', path.join('./', `${this.conf.pathId}.mp4`)); 43 | } 44 | 45 | /** 46 | * Get the val corresponding to the key 47 | * @param {string} key - key 48 | * @return {any} val 49 | * @public 50 | */ 51 | getVal(key) { 52 | if (key === 'detailedCacheDir') return this.getCacheDir(); 53 | return this.conf[key]; 54 | } 55 | 56 | /** 57 | * Set the val corresponding to the key 58 | * @param {string} key - key 59 | * @param {any} val - val 60 | * @public 61 | */ 62 | setVal(key, val) { 63 | this.conf[key] = val; 64 | } 65 | 66 | /** 67 | * Get the width and height in the configuration (add separator) 68 | * @param {string} dot - separator 69 | * @retrun {string} 'widthxheight' 70 | * @public 71 | */ 72 | getWH(dot = 'x') { 73 | return this.getVal('width') + dot + this.getVal('height'); 74 | } 75 | 76 | /** 77 | * Get the cache directory 78 | * @retrun {string} path 79 | * @public 80 | */ 81 | getCacheDir() { 82 | let cacheDir = this.getVal('cacheDir'); 83 | let pathId = this.getVal('pathId'); 84 | return path.join(cacheDir, pathId); 85 | } 86 | 87 | copyByDefaultVal(conf, key, defalutVal) { 88 | this.conf[key] = conf[key] === undefined ? defalutVal : conf[key]; 89 | } 90 | 91 | /** 92 | * Guarantee that a key must have value 93 | * @public 94 | */ 95 | copyFromMultipleVal(conf, key, otherKey, defalutVal) { 96 | this.conf[key] = conf[key] || conf[otherKey] || defalutVal; 97 | } 98 | 99 | /** 100 | * A fake proxy Conf object 101 | * @public 102 | */ 103 | static getFakeConf() { 104 | return fakeConf; 105 | } 106 | } 107 | 108 | const fakeConf = { 109 | // eslint-disable-next-line 110 | getVal(key) { 111 | return null; 112 | }, 113 | // eslint-disable-next-line 114 | setVal(key, val) { 115 | return null; 116 | }, 117 | }; 118 | 119 | module.exports = Conf; 120 | -------------------------------------------------------------------------------- /lib/core/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFBase - Basic classes in FFCreatorLite. Note: Its subclass is not necessarily a display class. 5 | * 6 | * ####Example: 7 | * 8 | * class FFCon extends FFBase 9 | * 10 | * @class 11 | */ 12 | const EventEmitter = require('eventemitter3'); 13 | const Conf = require('../conf/conf'); 14 | const Utils = require('../utils/utils'); 15 | 16 | class FFBase extends EventEmitter { 17 | constructor(conf) { 18 | super(); 19 | 20 | this.conf = { type: 'base', ...conf }; 21 | this.type = this.conf.type; 22 | this.parent = null; 23 | this.genId(); 24 | } 25 | 26 | /** 27 | * Generate self-increasing unique id 28 | * @return {string} unique id 29 | * @public 30 | */ 31 | genId() { 32 | const { type } = this.conf; 33 | this.id = Utils.genId(type); 34 | } 35 | 36 | /** 37 | * Get the logical root node of the instance 38 | * @return {FFBase} root node 39 | * @public 40 | */ 41 | root() { 42 | if (this.parent) return this.parent.root(); 43 | else return this; 44 | } 45 | 46 | /** 47 | * Get the conf configuration on the logical root node of the instance 48 | * If the val parameter is set, the val value of conf is set 49 | * @param {string} key - configuration key 50 | * @param {any} val - configuration val 51 | * @return {object|any} root node 52 | * @public 53 | */ 54 | rootConf(key, val) { 55 | let conf = Conf.getFakeConf(); 56 | const root = this.root(); 57 | if (root && root.type === 'creator') conf = root.conf; 58 | 59 | if (key) { 60 | if (val !== undefined) conf.setVal(key, val); 61 | return conf.getVal(key); 62 | } else { 63 | return conf; 64 | } 65 | } 66 | 67 | /** 68 | * Event dispatch function 69 | * @public 70 | */ 71 | emits(event) { 72 | this.emit(event.type, event); 73 | } 74 | 75 | emitsClone(type, event) { 76 | event = Utils.clone(event); 77 | event.type = type; 78 | this.emits(event); 79 | } 80 | 81 | /** 82 | * Destroy the component 83 | * @public 84 | */ 85 | destroy() { 86 | this.parent = null; 87 | this.removeAllListeners(); 88 | } 89 | } 90 | 91 | module.exports = FFBase; 92 | -------------------------------------------------------------------------------- /lib/core/center.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFCreatorCenter - A global FFCreator task scheduling center. 5 | * You don’t have to use it, you can easily implement a task manager by yourself. 6 | * 7 | * ####Example: 8 | * 9 | * FFCreatorCenter.addTask(()=>{ 10 | * const creator = new FFCreator; 11 | * return creator; 12 | * }); 13 | * 14 | * 15 | * ####Note: 16 | * On the server side, you only need to start FFCreatorCenter, 17 | * remember to add error logs to the events in it 18 | * 19 | * @object 20 | */ 21 | 22 | const EventEmitter = require('eventemitter3'); 23 | const forEach = require('lodash/forEach'); 24 | const cloneDeep = require('lodash/cloneDeep'); 25 | const Utils = require('../utils/utils'); 26 | const FFmpegUtil = require('../utils/ffmpeg'); 27 | 28 | /** 29 | * TaskQueue - Task queue, representing a production task 30 | * @class 31 | */ 32 | class TaskQueue { 33 | constructor() { 34 | this.queue = []; 35 | } 36 | 37 | /** 38 | * Add a subtask to the end of the task queue 39 | * @param {function} task - a task handler function 40 | * @return {string} task id 41 | * @public 42 | */ 43 | push(task) { 44 | const id = Utils.uid(); 45 | this.queue.push({ id, task, state: 'waiting', events: {} }); 46 | return id; 47 | } 48 | 49 | /** 50 | * Delete a task from the task queue 51 | * @param {object} taskObj - a task config object 52 | * @return {number} the task index 53 | * @public 54 | */ 55 | remove(taskObj) { 56 | const index = this.queue.indexOf(taskObj); 57 | if (index > -1) { 58 | this.queue.splice(index, 1); 59 | } 60 | return index; 61 | } 62 | 63 | /** 64 | * Clear all tasks in the queue 65 | * @public 66 | */ 67 | clear() { 68 | forEach(this.queue, taskObj => setTimeout(() => Utils.destroyObj(taskObj), 15)); 69 | this.queue.length = 0; 70 | } 71 | 72 | getLength() { 73 | return this.queue.length; 74 | } 75 | 76 | /** 77 | * Get the status of a task by id 78 | * @public 79 | */ 80 | getTaskState(id) { 81 | const taskObj = this.getTaskById(id); 82 | return taskObj ? taskObj.state : 'unknown'; 83 | } 84 | 85 | /** 86 | * Get a taskObj by id 87 | * @public 88 | */ 89 | getTaskById(id) { 90 | for (let taskObj of this.queue) { 91 | if (id === taskObj.id) return taskObj; 92 | } 93 | 94 | return null; 95 | } 96 | 97 | /** 98 | * Get a taskObj by index 99 | * @public 100 | */ 101 | getHeadTask() { 102 | if (this.queue.length > 0) return this.queue[0]; 103 | return null; 104 | } 105 | 106 | addListener(id, name, func) { 107 | const taskObj = this.getTaskById(id); 108 | if (taskObj) { 109 | taskObj.events[name] = func; 110 | } 111 | } 112 | 113 | getListener(id, name) { 114 | const taskObj = this.getTaskById(id); 115 | if (taskObj) { 116 | return taskObj.events[name]; 117 | } 118 | return null; 119 | } 120 | } 121 | 122 | /** 123 | * Progress - A class used to calculate the production progress 124 | * @class 125 | */ 126 | class Progress { 127 | constructor(max = 30) { 128 | this.id = -1; 129 | this.ids = []; 130 | this.percent = 0; 131 | this.max = max; 132 | } 133 | 134 | add(id) { 135 | this.ids.push(id); 136 | if (this.ids.length > this.max) this.ids.shift(); 137 | } 138 | 139 | getPercent(id) { 140 | if (this.id === id) { 141 | return this.percent; 142 | } else if (this.ids.indexOf(id) > -1) { 143 | return 1; 144 | } else { 145 | return 0; 146 | } 147 | } 148 | } 149 | 150 | /** 151 | * FFCreatorCenter - A global FFCreator task scheduling center. 152 | * @object 153 | */ 154 | const FFCreatorCenter = { 155 | cursor: 0, 156 | delay: 500, 157 | state: 'free', 158 | progress: new Progress(), 159 | event: new EventEmitter(), 160 | taskQueue: new TaskQueue(), 161 | 162 | /** 163 | * Add a production task 164 | * @param {function} task - a production task 165 | * @public 166 | */ 167 | addTask(task) { 168 | const id = this.taskQueue.push(task); 169 | if (this.state === 'free') this.start(); 170 | return id; 171 | }, 172 | 173 | /** 174 | * Listen to production task events 175 | * @param {string} id - a task id 176 | * @param {strint} eventName - task name 177 | * @param {function} func - task event handler 178 | * @public 179 | */ 180 | onTask(id, eventName, func) { 181 | const { event, taskQueue } = this; 182 | taskQueue.addListener(id, eventName, func); 183 | if (event.listenerCount(eventName) <= 0) event.on(eventName, this.eventHandler.bind(this)); 184 | }, 185 | 186 | eventHandler(result) { 187 | let cresult = result; 188 | const { taskQueue } = this; 189 | const { id, eventName } = result; 190 | const taskObj = taskQueue.getTaskById(id); 191 | if (!taskObj) return; 192 | 193 | const func = taskQueue.getListener(id, eventName); 194 | if (eventName !== 'single-progress') cresult = cloneDeep(result); 195 | func(cresult); 196 | }, 197 | 198 | removeTaskObj(id) { 199 | const { taskQueue } = this; 200 | const taskObj = taskQueue.getTaskById(id); 201 | if (!taskObj) return; 202 | 203 | Utils.destroyObj(taskObj['events']); 204 | Utils.destroyObj(taskObj); 205 | this.taskQueue.remove(taskObj); 206 | }, 207 | 208 | /** 209 | * Listen to production task Error events 210 | * @param {string} id - a task id 211 | * @param {function} func - task event handler 212 | * @public 213 | */ 214 | onTaskError(id, func) { 215 | this.onTask(id, 'single-error', func); 216 | }, 217 | 218 | /** 219 | * Listen to production task Complete events 220 | * @param {string} id - a task id 221 | * @param {function} func - task event handler 222 | * @public 223 | */ 224 | onTaskComplete(id, func) { 225 | this.onTask(id, 'single-complete', func); 226 | }, 227 | 228 | /** 229 | * Listen to production task Start events 230 | * @param {string} id - a task id 231 | * @param {function} func - task event handler 232 | * @public 233 | */ 234 | onTaskStart(id, func) { 235 | this.onTask(id, 'single-start', func); 236 | }, 237 | /** 238 | * Listen to production task Complete events 239 | * @param {string} id - a task id 240 | * @param {function} func - task event handler 241 | * @public 242 | */ 243 | onTaskProgress(id, func) { 244 | this.onTask(id, 'single-progress', func); 245 | }, 246 | 247 | /** 248 | * Start a task 249 | * @async 250 | * @public 251 | */ 252 | async start() { 253 | const taskObj = this.taskQueue.getHeadTask(); 254 | this.execTask(taskObj); 255 | }, 256 | 257 | /** 258 | * Get the status of a task by id 259 | * @public 260 | */ 261 | getTaskState(id) { 262 | return this.taskQueue.getTaskState(id); 263 | }, 264 | 265 | getProgress(id) { 266 | return this.progress.getPercent(id); 267 | }, 268 | 269 | async execTask(taskObj) { 270 | this.state = 'busy'; 271 | try { 272 | const creator = await taskObj.task(taskObj.id); 273 | if (!creator) { 274 | this.handlingError({ 275 | taskObj, 276 | error: 'execTask: await taskObj.task(taskObj.id) return null', 277 | }); 278 | } else { 279 | this.initCreator(creator, taskObj); 280 | } 281 | } catch (error) { 282 | console.error(error); 283 | this.handlingError({ taskObj, error }); 284 | } 285 | }, 286 | 287 | initCreator(creator, taskObj) { 288 | const { id } = taskObj; 289 | creator.inCenter = true; 290 | creator.taskId = id; 291 | creator.generateOutput(); 292 | 293 | // event listeners 294 | creator.on('start', () => { 295 | const eventName = 'single-start'; 296 | const result = { id, eventName }; 297 | this.event.emit(eventName, result); 298 | }); 299 | 300 | creator.on('error', event => { 301 | this.handlingError({ taskObj, error: event }); 302 | }); 303 | 304 | creator.on('progress', p => { 305 | const { progress } = this; 306 | const eventName = 'single-progress'; 307 | progress.id = id; 308 | progress.state = p.state; 309 | progress.percent = p.percent; 310 | progress.eventName = eventName; 311 | this.event.emit(eventName, progress); 312 | }); 313 | 314 | creator.on('complete', () => { 315 | try { 316 | const eventName = 'single-complete'; 317 | this.progress.add(id); 318 | taskObj.state = 'complete'; 319 | const file = creator.getFile(); 320 | const result = { id, file, output: file, eventName }; 321 | this.event.emit(eventName, result); 322 | } catch (e) {} 323 | 324 | setTimeout(id => this.removeTaskObj(id), 50, id); 325 | setTimeout(this.nextTask.bind(this), this.delay); 326 | }); 327 | }, 328 | 329 | handlingError({ taskObj, error = 'normal' }) { 330 | const { id } = taskObj; 331 | const eventName = 'single-error'; 332 | taskObj.state = 'error'; 333 | const result = { id, error, eventName }; 334 | this.event.emit(eventName, result); 335 | 336 | setTimeout(id => this.removeTaskObj(id), 50, id); 337 | setTimeout(this.nextTask.bind(this), this.delay); 338 | }, 339 | 340 | nextTask() { 341 | if (this.taskQueue.getLength() <= 0) { 342 | this.resetTasks(); 343 | this.event.emit('all-complete'); 344 | } else { 345 | const taskObj = this.taskQueue.getHeadTask(); 346 | this.execTask(taskObj); 347 | } 348 | }, 349 | 350 | resetTasks() { 351 | this.state = 'free'; 352 | this.taskQueue.clear(); 353 | }, 354 | 355 | /** 356 | * Set the installation path of the current server ffmpeg. 357 | * If not set, the ffmpeg command of command will be found by default. 358 | * 359 | * @param {string} path - installation path of the current server ffmpeg 360 | * @public 361 | */ 362 | setFFmpegPath(path) { 363 | FFmpegUtil.setFFmpegPath(path); 364 | }, 365 | 366 | /** 367 | * Set the installation path of the current server ffprobe. 368 | * If not set, the ffprobe command of command will be found by default. 369 | * 370 | * @param {string} path - installation path of the current server ffprobe 371 | * @public 372 | */ 373 | setFFprobePath(path) { 374 | FFmpegUtil.setFFprobePath(path); 375 | }, 376 | }; 377 | 378 | module.exports = FFCreatorCenter; 379 | -------------------------------------------------------------------------------- /lib/core/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFContext 5 | * 6 | * @class 7 | */ 8 | class FFContext { 9 | constructor() { 10 | this.input = null; 11 | this.output = null; 12 | this.inFilters = false; 13 | } 14 | } 15 | 16 | module.exports = FFContext; 17 | -------------------------------------------------------------------------------- /lib/core/renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Renderer - Core classes for rendering animations and videos. 5 | * 6 | * ####Example: 7 | * 8 | * const renderer = new Renderer({ queue: 2 }); 9 | * 10 | * 11 | * @class 12 | */ 13 | const FFBase = require('./base'); 14 | const Perf = require('../utils/perf'); 15 | const Utils = require('../utils/utils'); 16 | const ScenesUtil = require('../utils/scenes'); 17 | const Synthesis = require('./synthesis'); 18 | const forEach = require('lodash/forEach'); 19 | 20 | class Renderer extends FFBase { 21 | constructor({ queue = 2, creator }) { 22 | super({ type: 'renderer' }); 23 | 24 | this.creator = creator; 25 | this.queue = queue; 26 | this.cursor = 0; 27 | this.percent = 0; 28 | } 29 | 30 | /** 31 | * Start rendering 32 | * @async 33 | * @public 34 | */ 35 | start() { 36 | const { creator } = this; 37 | const log = creator.getConf('log'); 38 | const upStreaming = creator.getConf('upStreaming'); 39 | 40 | Perf.setEnabled(log); 41 | Perf.start(); 42 | 43 | const { scenes } = creator; 44 | if (ScenesUtil.isSingle(creator) || upStreaming) { 45 | const scene = scenes[0]; 46 | scene.addAudio(); 47 | this.singleStart(scene); 48 | } else { 49 | for (let i = this.cursor; i < this.cursor + this.queue; i++) { 50 | this.singleStart(scenes[i]); 51 | } 52 | } 53 | } 54 | 55 | async singleStart(scene) { 56 | if (!scene) return; 57 | 58 | await scene.isReady(); 59 | scene.on('single-start', this.eventHandler.bind(this)); 60 | scene.on('single-error', this.eventHandler.bind(this)); 61 | scene.on('single-progress', this.eventHandler.bind(this)); 62 | scene.on('single-complete', this.eventHandler.bind(this)); 63 | scene.start(); 64 | } 65 | 66 | nextStart() { 67 | const { scenes } = this.creator; 68 | const index = this.cursor + this.queue - 1; 69 | if (index >= scenes.length) return; 70 | 71 | const scene = scenes[index]; 72 | this.singleStart(scene); 73 | } 74 | 75 | eventHandler(event) { 76 | event = Utils.clone(event); 77 | const baseline = ScenesUtil.isSingle(this.creator) ? 1 : 0.7; 78 | 79 | switch (event.type) { 80 | case 'single-error': 81 | case 'synthesis-error': 82 | event.type = 'error'; 83 | this.emits(event); 84 | break; 85 | 86 | case 'single-progress': 87 | event.type = 'progress'; 88 | event.percent = this.percent = this.getPercent(event) * baseline; 89 | this.emits(event); 90 | break; 91 | 92 | case 'single-complete': 93 | this.cursor++; 94 | this.checkCompleted(event); 95 | break; 96 | } 97 | } 98 | 99 | getPercent(event) { 100 | let percent = 0; 101 | const { scenes } = this.creator; 102 | const scene = event.target; 103 | scene.percent = event.fpercent; 104 | forEach(scenes, scene => (percent += scene.percent)); 105 | percent /= scenes.length; 106 | 107 | return percent; 108 | } 109 | 110 | checkCompleted(event) { 111 | const { scenes } = this.creator; 112 | if (this.cursor >= scenes.length) { 113 | this.synthesisOutput(); 114 | } else { 115 | this.emits(event); 116 | this.nextStart(); 117 | } 118 | } 119 | 120 | /** 121 | * synthesis Video Function 122 | * @private 123 | */ 124 | async synthesisOutput() { 125 | const { creator } = this; 126 | const synthesis = new Synthesis(creator); 127 | synthesis.on('synthesis-error', this.eventHandler.bind(this)); 128 | synthesis.on('synthesis-complete', event => { 129 | Perf.end(); 130 | event.useage = Perf.getInfo(); 131 | event.percent = 1; 132 | this.emit('all-complete', event); 133 | }); 134 | synthesis.on('synthesis-progress', event => { 135 | event.type = 'progress'; 136 | event.percent = this.percent + event.percent * 0.3; 137 | this.emits(event); 138 | }); 139 | 140 | synthesis.start(); 141 | } 142 | 143 | destroy() { 144 | this.synthesis.destroy(); 145 | this.removeAllListeners(); 146 | super.destroy(); 147 | this.synthesis = null; 148 | this.creator = null; 149 | } 150 | } 151 | 152 | module.exports = Renderer; 153 | -------------------------------------------------------------------------------- /lib/core/synthesis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Synthesis - A class for video synthesis. 5 | * Mainly rely on the function of ffmpeg to synthesize video and audio. 6 | * 7 | * ####Example: 8 | * 9 | * const synthesis = new Synthesis(conf); 10 | * synthesis.start(); 11 | * 12 | * 13 | * @class 14 | */ 15 | const path = require('path'); 16 | const rmfr = require('rmfr'); 17 | const { promisify } = require('util'); 18 | const fs = require('fs-extra'); 19 | const FFBase = require('./base'); 20 | const isEmpty = require('lodash/isEmpty'); 21 | const forEach = require('lodash/forEach'); 22 | const Utils = require('../utils/utils'); 23 | const ScenesUtil = require('../utils/scenes'); 24 | const FFmpegUtil = require('../utils/ffmpeg'); 25 | const writeFileAsync = promisify(fs.writeFile); 26 | 27 | class Synthesis extends FFBase { 28 | constructor(creator) { 29 | super({ type: 'synthesis' }); 30 | 31 | this.creator = creator; 32 | this.conf = creator.rootConf(); 33 | this.inputOptions = []; 34 | this.outputOptions = []; 35 | this.txtpath = ''; 36 | this.command = FFmpegUtil.createCommand(); 37 | } 38 | 39 | /** 40 | * Open ffmpeg production and processing 41 | * @public 42 | */ 43 | async start() { 44 | const { creator, conf } = this; 45 | const upStreaming = conf.getVal('upStreaming'); 46 | 47 | if (upStreaming) { 48 | this.liveOutput(); 49 | } else if (ScenesUtil.isSingle(creator)) { 50 | this.mvOutput(); 51 | } else { 52 | if (ScenesUtil.hasTransition(creator)) { 53 | ScenesUtil.fillTransition(creator); 54 | this.addXfadeInput(); 55 | } else { 56 | await this.addConcatInput(); 57 | } 58 | 59 | this.addAudio(); 60 | this.addOutputOptions(); 61 | this.addCommandEvents(); 62 | this.addOutput(); 63 | this.command.run(); 64 | } 65 | } 66 | 67 | liveOutput() { 68 | const { conf } = this; 69 | const output = conf.getVal('output'); 70 | this.emits({ type: 'synthesis-complete', path: output, output }); 71 | } 72 | 73 | /** 74 | * Produce final documents and move and save 75 | * @private 76 | */ 77 | async mvOutput() { 78 | const { conf, creator } = this; 79 | const { scenes } = creator; 80 | 81 | const scene = scenes[0]; 82 | const cacheFile = scene.getFile(); 83 | const output = conf.getVal('output'); 84 | await fs.move(cacheFile, output); 85 | 86 | const debug = conf.getVal('debug'); 87 | if (!debug) this.deleteCacheFile(); 88 | 89 | this.emits({ type: 'synthesis-complete', path: output, output }); 90 | } 91 | 92 | /** 93 | * add input by xfade filter i case 94 | * @private 95 | */ 96 | addXfadeInput() { 97 | const { scenes } = this.creator; 98 | const filters = []; 99 | let offset = 0; 100 | let cid; 101 | 102 | forEach(scenes, (scene, index) => { 103 | const file = scene.getFile(); 104 | this.command.addInput(file); 105 | 106 | if (index >= scenes.length - 1) return; 107 | offset += scene.getNormalDuration(); 108 | const filter = scene.toTransFilter(offset); 109 | 110 | cid = cid || `${index}:v`; 111 | const nid = `${index + 1}:v`; 112 | filter.inputs = [cid, nid]; 113 | cid = `scene-${index}`; 114 | filter.outputs = [cid]; 115 | filters.push(filter); 116 | }); 117 | 118 | this.command.complexFilter(filters, cid); 119 | } 120 | 121 | /** 122 | * add input by concat multiple videos case 123 | * @private 124 | */ 125 | async addConcatInput() { 126 | // create path txt 127 | const { creator } = this; 128 | const cacheDir = creator.rootConf('cacheDir').replace(/\/$/, ''); 129 | this.txtpath = path.join(cacheDir, `${Utils.uid()}.txt`); 130 | 131 | let text = ''; 132 | forEach(creator.scenes, scene => (text += `file '${scene.getFile()}'\n`)); 133 | await writeFileAsync(this.txtpath, text, { 134 | encoding: 'utf8' 135 | }); 136 | 137 | // Add the intermediate pictures processed in the cache directory to ffmpeg input 138 | this.command.addInput(this.txtpath); 139 | this.command.inputOptions(['-f', 'concat', '-safe', '0']); 140 | } 141 | 142 | /** 143 | * Add one background sounds 144 | * @param {array} audio - background sounds 145 | * @public 146 | */ 147 | addAudio() { 148 | const { conf, command } = this; 149 | const audio = conf.getVal('audio'); 150 | if (isEmpty(audio)) return; 151 | 152 | command.addInput(audio); 153 | command.inputOptions(['-stream_loop', '-1']); 154 | } 155 | 156 | /** 157 | * Get default ffmpeg output configuration 158 | * @private 159 | */ 160 | getDefaultOutputOptions() { 161 | const { conf } = this; 162 | const fps = conf.getVal('fps'); 163 | const crf = conf.getVal('crf'); 164 | const opts = [] 165 | // misc 166 | .concat([ 167 | '-hide_banner', // hide_banner - parameter, you can display only meta information 168 | '-map_metadata', 169 | '-1', 170 | '-map_chapters', 171 | '-1', 172 | ]) 173 | 174 | // video 175 | .concat([ 176 | '-c', 177 | 'copy', 178 | '-c:v', 179 | 'libx264', // c:v - H.264 180 | '-profile:v', 181 | 'main', // profile:v - main profile: mainstream image quality. Provide I / P / B frames 182 | '-preset', 183 | 'medium', // preset - compromised encoding speed 184 | '-crf', 185 | crf, // crf - The range of quantization ratio is 0 ~ 51, where 0 is lossless mode, 23 is the default value, 51 may be the worst 186 | '-movflags', 187 | 'faststart', 188 | '-pix_fmt', 189 | 'yuv420p', 190 | '-r', 191 | fps, 192 | ]); 193 | 194 | return opts; 195 | } 196 | 197 | /** 198 | * Add ffmpeg output configuration 199 | * @private 200 | */ 201 | addOutputOptions() { 202 | const { conf, creator } = this; 203 | const outputOptions = []; 204 | // default 205 | const defaultOpts = this.getDefaultOutputOptions(conf); 206 | FFmpegUtil.concatOpts(outputOptions, defaultOpts); 207 | 208 | // audio 209 | const audio = conf.getVal('audio'); 210 | if (!isEmpty(audio)) { 211 | let apro = ''; 212 | if (ScenesUtil.hasTransition(creator)) { 213 | const index = ScenesUtil.getLength(creator); 214 | apro += `-map ${index}:a `; 215 | } 216 | 217 | FFmpegUtil.concatOpts(outputOptions, `${apro}-c:a aac -shortest`.split(' ')); 218 | } 219 | this.command.outputOptions(outputOptions); 220 | } 221 | 222 | /** 223 | * Set ffmpeg input path 224 | * @private 225 | */ 226 | addOutput() { 227 | const { conf } = this; 228 | const output = conf.getVal('output'); 229 | const dir = path.dirname(output); 230 | fs.ensureDir(dir); 231 | this.command.output(output); 232 | } 233 | 234 | /** 235 | * Add FFmpeg event to command 236 | * @private 237 | */ 238 | addCommandEvents() { 239 | const { conf, command, creator } = this; 240 | const totalFrames = creator.getTotalFrames(); 241 | const debug = conf.getVal('debug'); 242 | 243 | // start 244 | command.on('start', commandLine => { 245 | const log = conf.getVal('log'); 246 | if (log) console.log(commandLine); 247 | this.emits({ type: 'synthesis-start', command: commandLine }); 248 | }); 249 | 250 | // progress 251 | command.on('progress', progress => { 252 | const percent = progress.frames / totalFrames; 253 | this.emits({ type: 'synthesis-progress', percent }); 254 | }); 255 | 256 | // complete 257 | command.on('end', () => { 258 | if (!debug) this.deleteCacheFile(); 259 | const output = conf.getVal('output'); 260 | this.emits({ type: 'synthesis-complete', path: output, output }); 261 | }); 262 | 263 | // error 264 | command.on('error', (error, stdout, stderr) => { 265 | if (!debug) this.deleteCacheFile(); 266 | // const log = conf.getVal('log'); 267 | // if (logFFmpegError) 268 | 269 | this.emits({ 270 | type: 'synthesis-error', 271 | error: `${error} \n stdout: ${stdout} \n stderr: ${stderr}`, 272 | pos: 'Synthesis', 273 | }); 274 | }); 275 | } 276 | 277 | deleteCacheFile() { 278 | if (this.txtpath) rmfr(this.txtpath); 279 | } 280 | 281 | destroy() { 282 | this.conf = null; 283 | this.creator = null; 284 | this.command = null; 285 | super.destroy(); 286 | } 287 | } 288 | 289 | module.exports = Synthesis; 290 | -------------------------------------------------------------------------------- /lib/creator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFCreator - FFCreatorLite main class, a container contains multiple scenes and pictures, etc. 5 | * Can be used alone, more often combined with FFCreatorCenter. 6 | * 7 | * ####Example: 8 | * 9 | * const creator = new FFCreator({ cacheDir, outputDir, width: 800, height: 640, audio }); 10 | * creator.addChild(scene); 11 | * creator.output(output); 12 | * creator.openLog(); 13 | * creator.start(); 14 | * 15 | * 16 | * ####Note: 17 | * The library depends on `ffmpeg` and `webgl` (linux server uses headless-webgl). 18 | * 19 | * @class 20 | */ 21 | const forEach = require('lodash/forEach'); 22 | const FFCon = require('./node/cons'); 23 | const Conf = require('./conf/conf'); 24 | const Utils = require('./utils/utils'); 25 | const Renderer = require('./core/renderer'); 26 | const FFmpegUtil = require('./utils/ffmpeg'); 27 | 28 | class FFCreator extends FFCon { 29 | constructor(conf = {}) { 30 | super({ type: 'creator', ...conf }); 31 | 32 | this.conf = new Conf(conf); 33 | const queue = this.getConf('queue'); 34 | this.renderer = new Renderer({ queue, creator: this }); 35 | 36 | this.closeLog(); 37 | this.scenes = []; 38 | this.taskId = null; 39 | this.inCenter = false; 40 | } 41 | 42 | /** 43 | * Set the fps of the composite video. 44 | * @param {number} fps - the fps of the composite video 45 | * @public 46 | */ 47 | setFps(fps) { 48 | this.setConf('fps', fps); 49 | } 50 | 51 | /** 52 | * Set configuration. 53 | * @param {string} key - the config key 54 | * @param {any} val - the config val 55 | * @public 56 | */ 57 | setConf(key, val) { 58 | this.conf.setVal(key, val); 59 | } 60 | 61 | /** 62 | * Get configuration. 63 | * @param {string} key - the config key 64 | * @return {any} the config val 65 | * @public 66 | */ 67 | getConf(key) { 68 | return this.conf.getVal(key); 69 | } 70 | 71 | /** 72 | * Set the stage size of the scene 73 | * @param {number} width - stage width 74 | * @param {number} height - stage height 75 | * @public 76 | */ 77 | setSize(w, h) { 78 | this.setConf('w', w); 79 | this.setConf('h', h); 80 | } 81 | 82 | /** 83 | * Add background sound. 84 | * @param {string|objecg} args - the audio config 85 | * @public 86 | */ 87 | addAudio(audio) { 88 | this.setConf('audio', audio); 89 | } 90 | 91 | /** 92 | * Set the video output path 93 | * @param {string} output - the video output path 94 | * @public 95 | */ 96 | setOutput(output) { 97 | this.setConf('output', output); 98 | } 99 | 100 | /** 101 | * Get Current ffmpeg version 102 | * @return {string} current ffmpeg version 103 | * @public 104 | */ 105 | async getFFmpegVersion() { 106 | return await FFmpegUtil.getVersion(); 107 | } 108 | 109 | getOutput() { 110 | return this.getConf('output'); 111 | } 112 | 113 | /** 114 | * Open logger switch 115 | * @public 116 | */ 117 | openLog() { 118 | this.setConf('log', true); 119 | } 120 | 121 | /** 122 | * Close logger switch 123 | * @public 124 | */ 125 | closeLog() { 126 | this.setConf('log', false); 127 | } 128 | 129 | addChild(child) { 130 | this.scenes.push(child); 131 | super.addChild(child); 132 | } 133 | 134 | async start() { 135 | await Utils.sleep(20); 136 | 137 | this.emit('start'); 138 | const { renderer } = this; 139 | renderer.on('error', event => { 140 | this.emitsClone('error', event); 141 | this.deleteAllCacheFile(); 142 | }); 143 | 144 | renderer.on('progress', event => { 145 | this.emitsClone('progress', event); 146 | }); 147 | 148 | renderer.on('all-complete', event => { 149 | if (this.inCenter) { 150 | event.taskId = this.taskId; 151 | } 152 | 153 | event.creator = this.id; 154 | this.emitsClone('complete', event); 155 | this.deleteAllCacheFile(); 156 | }); 157 | 158 | renderer.start(); 159 | } 160 | 161 | getTotalFrames() { 162 | let frames = 0; 163 | forEach(this.scenes, scene => (frames += scene.getTotalFrames())); 164 | return frames; 165 | } 166 | 167 | /** 168 | * Create output path, only used when using FFCreatorCenter. 169 | * @public 170 | */ 171 | generateOutput() { 172 | const upStreaming = this.getConf('upStreaming'); 173 | let outputDir = this.getConf('outputDir'); 174 | 175 | if (this.inCenter && outputDir && !upStreaming) { 176 | outputDir = outputDir.replace(/\/$/, ''); 177 | const output = `${outputDir}/${Utils.uid()}.mp4`; 178 | this.setConf('output', output); 179 | } 180 | } 181 | 182 | /** 183 | * Get the video output path 184 | * @return {string} output - the video output path 185 | * @public 186 | */ 187 | getFile() { 188 | return this.getConf('output'); 189 | } 190 | 191 | /** 192 | * delete All Cache File 193 | * @private 194 | */ 195 | deleteAllCacheFile() { 196 | forEach(this.scenes, scene => scene.deleteCacheFile()); 197 | } 198 | 199 | destroy() { 200 | super.destroy(); 201 | this.renderer.destroy(); 202 | forEach(this.scenes, scene => scene.destroy()); 203 | } 204 | 205 | /** 206 | * Set the installation path of the current server ffmpeg. 207 | * @param {string} path - installation path of the current server ffmpeg 208 | * @public 209 | */ 210 | static setFFmpegPath(path) { 211 | FFmpegUtil.setFFmpegPath(path); 212 | } 213 | 214 | /** 215 | * Set the installation path of the current server ffprobe. 216 | * @param {string} path - installation path of the current server ffprobe 217 | * @public 218 | */ 219 | static setFFprobePath(path) { 220 | FFmpegUtil.setFFprobePath(path); 221 | } 222 | } 223 | 224 | module.exports = FFCreator; 225 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /*! 4 | * FFCreatorLite - a lightweight and flexible short video production library. 5 | * Copyright(c) TNTWeb Team 6 | * 7 | * Licensed under the MIT license: 8 | * http://www.opensource.org/licenses/mit-license.php 9 | */ 10 | 11 | const FFCreator = require('./creator'); 12 | const FFContext = require('./core/context'); 13 | const FFCreatorCenter = require('./core/center'); 14 | const FFText = require('./node/text'); 15 | const FFImage = require('./node/image'); 16 | const FFVideo = require('./node/video'); 17 | const FFLive = require('./node/live'); 18 | const FFScene = require('./node/scene'); 19 | const FFBackGround = require('./node/background'); 20 | 21 | module.exports = { 22 | FFCreator, 23 | FFImage, 24 | FFLive, 25 | FFText, 26 | FFVideo, 27 | FFScene, 28 | FFContext, 29 | FFBackGround, 30 | FFCreatorCenter, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/math/ease.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Ease 5 | * 6 | * ####Note: 7 | * 8 | * The source of these formulas is 9 | * https://github.com/danro/jquery-easing/blob/master/jquery.easing.js 10 | * Limited to CPU computing power, there is no more ease function. 11 | * 12 | * Arguments 13 | * t,b,c,d,e - time, start, change(end-start), totalTime, delay 14 | * 15 | * @object 16 | */ 17 | 18 | const Ease = { 19 | getVal(type, b, c, d, e) { 20 | if (this[type]) return this[type](b, c, d, e); 21 | else return this.linear(b, c, d, e); 22 | }, 23 | 24 | linear(b, c, d, e) { 25 | const x = `(t-${e})/${d}`; 26 | return `${b}+${c}*${x}`; 27 | }, 28 | 29 | quadIn(b, c, d, e) { 30 | // c*(t/=d)*t + b; 31 | const x = `(t-${e})/${d}`; 32 | const xx = `pow(${x},2)`; 33 | return `${b}+${c}*${xx}`; 34 | }, 35 | 36 | quadOut(b, c, d, e) { 37 | const c2 = 2 * c; 38 | const x = `(t-${e})/${d}`; 39 | const xx = `pow(${x},2)`; 40 | // -c*x^2 + 2*x*c + b 41 | return `${b}+${c2}*${x}-${c}*${xx}`; 42 | }, 43 | 44 | backIn(b, c, d, e) { 45 | // c*(t/=d)*t*((s+1)*t - s) + b; 46 | const s = 1.7016; 47 | const cs = c * s; 48 | const cs1 = c * (s + 1); 49 | const x = `(t-${e})/${d}`; 50 | const xx = `pow(${x},2)`; 51 | const xxx = `pow(${x},3)`; 52 | return `${cs1}*${xxx}-${cs}*${xx}+${b}`; 53 | }, 54 | 55 | backOut(b, c, d, e) { 56 | // c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; 57 | const s = 1.7016; 58 | const cs = c * s; 59 | const cs1 = c * (s + 1); 60 | const x = `(t-${e})/${d}-1`; 61 | const xx = `pow(${x},2)`; 62 | const xxx = `pow(${x},3)`; 63 | return `${cs1}*${xxx}+${cs}*${xx}+${b}+${c}`; 64 | }, 65 | }; 66 | 67 | module.exports = Ease; 68 | -------------------------------------------------------------------------------- /lib/math/maths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Maths 5 | * @function 6 | */ 7 | 8 | function accAdd(arg1, arg2) { 9 | let r1, r2, m, c; 10 | try { 11 | r1 = arg1.toString().split('.')[1].length; 12 | } catch (e) { 13 | r1 = 0; 14 | } 15 | 16 | try { 17 | r2 = arg2.toString().split('.')[1].length; 18 | } catch (e) { 19 | r2 = 0; 20 | } 21 | 22 | c = Math.abs(r1 - r2); 23 | m = Math.pow(10, Math.max(r1, r2)); 24 | 25 | if (c > 0) { 26 | let cm = Math.pow(10, c); 27 | if (r1 > r2) { 28 | arg1 = Number(arg1.toString().replace('.', '')); 29 | arg2 = Number(arg2.toString().replace('.', '')) * cm; 30 | } else { 31 | arg1 = Number(arg1.toString().replace('.', '')) * cm; 32 | arg2 = Number(arg2.toString().replace('.', '')); 33 | } 34 | } else { 35 | arg1 = Number(arg1.toString().replace('.', '')); 36 | arg2 = Number(arg2.toString().replace('.', '')); 37 | } 38 | 39 | return (arg1 + arg2) / m; 40 | } 41 | 42 | module.exports = { accAdd }; 43 | -------------------------------------------------------------------------------- /lib/node/audio.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFVideo - Video component-based display component 5 | * 6 | * ####Example: 7 | * 8 | * const video = new FFVideo({ path, width: 500, height: 350, loop: true }); 9 | * scene.addChild(video); 10 | * 11 | * 12 | * @class 13 | */ 14 | const FFNode = require('./node'); 15 | 16 | class FFAudio extends FFNode { 17 | constructor(conf) { 18 | super({ type: 'audio', ...conf }); 19 | this.hasInput = true; 20 | } 21 | 22 | /** 23 | * Add video ffmpeg input 24 | * ex: loop 1 -t 20 -i imgs/logo.png 25 | * @private 26 | */ 27 | addInput(command) { 28 | const { audio, loop } = this.conf; 29 | command.addInput(audio); 30 | loop && command.inputOptions(['-stream_loop', '-1']); 31 | } 32 | 33 | getFId(k = false) { 34 | const vid = `${this.index}:a`; 35 | return k ? `[${vid}]` : `${vid}`; 36 | } 37 | 38 | setLoop(loop) { 39 | this.conf.loop = loop; 40 | } 41 | 42 | addOptions(command) { 43 | const inputs = this.getInId(); 44 | command.outputOptions(`-map ${inputs} -c:a aac -shortest`.split(' ')); 45 | } 46 | 47 | concatFilters() { 48 | return this.filters; 49 | } 50 | } 51 | 52 | module.exports = FFAudio; 53 | -------------------------------------------------------------------------------- /lib/node/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FFNode = require('./node'); 4 | 5 | class FFBackGround extends FFNode { 6 | constructor(conf = {}) { 7 | super({ type: 'background', ...conf }); 8 | 9 | this.color = conf.color; 10 | this.duration = conf.time || conf.duration || 999999; 11 | } 12 | 13 | /** 14 | * Combine various filters as ffmpeg parameters 15 | * @private 16 | */ 17 | concatFilters(context) { 18 | const filter = this.toFilter(context); 19 | this.filters.push(filter); 20 | return this.filters; 21 | } 22 | 23 | /** 24 | * Converted to ffmpeg filter command line parameters 25 | * @private 26 | */ 27 | toFilter(context) { 28 | const conf = this.rootConf(); 29 | const filter = { 30 | filter: 'color', 31 | options: { 32 | c: this.color, 33 | d: this.duration, 34 | size: conf.getWH('*'), 35 | }, 36 | outputs: this.getOutId(), 37 | }; 38 | 39 | context.input = this.getOutId(); 40 | return filter; 41 | } 42 | } 43 | 44 | module.exports = FFBackGround; 45 | -------------------------------------------------------------------------------- /lib/node/cons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFCon - display object container. 5 | * 6 | * ####Example: 7 | * 8 | * class FFScene extends FFCon 9 | * 10 | * @class 11 | */ 12 | const FFBase = require('../core/base'); 13 | const Utils = require('../utils/utils'); 14 | 15 | class FFCon extends FFBase { 16 | constructor(conf) { 17 | super({ type: 'con', ...conf }); 18 | this.children = []; 19 | this.filters = []; 20 | this.vLength = 0; 21 | this.hasInput = false; 22 | this.context = null; 23 | this.command = null; 24 | this.parent = null; 25 | } 26 | 27 | addChild(child) { 28 | if (child.hasInput) { 29 | const index = this.vLength++; 30 | child.index = index; 31 | this.context && (this.context.currentIndex = index); 32 | } 33 | 34 | child.parent = this; 35 | this.children.push(child); 36 | } 37 | 38 | addChildAt(child, index) { 39 | if (child.hasInput) { 40 | const index = this.vLength++; 41 | child.index = index; 42 | this.context && (this.context.currentIndex = index); 43 | } 44 | 45 | child.parent = this; 46 | this.children.splice(index, 0, child); 47 | } 48 | 49 | removeChild(child) { 50 | child.parent = null; 51 | Utils.deleteArrayElement(this.children, child); 52 | } 53 | 54 | swapChild(child1, child2) { 55 | Utils.swapArrayElement(this.children, child1, child2); 56 | } 57 | 58 | toCommand() {} 59 | 60 | destroy() { 61 | this.children.length = 0; 62 | } 63 | } 64 | 65 | module.exports = FFCon; 66 | -------------------------------------------------------------------------------- /lib/node/image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFImage - Image component-based display component 5 | * 6 | * ####Example: 7 | * 8 | * const img = new FFImage({ path, x: 94, y: 271, width: 375, height: 200, resetXY: true }); 9 | * img.addEffect("slideInDown", 1.2, 0); 10 | * scene.addChild(img); 11 | * 12 | * @class 13 | */ 14 | const sizeOf = require('image-size'); 15 | const FFNode = require('./node'); 16 | const Cache = require('../utils/cache'); 17 | 18 | class FFImage extends FFNode { 19 | constructor(conf = { x: 0, y: 0, animations: [] }) { 20 | super({ type: 'image', ...conf }); 21 | this.hasInput = true; 22 | // this.addImagePreFilter(); 23 | } 24 | 25 | /** 26 | * Add image preprocessing filter parameters 27 | * @public 28 | */ 29 | addImagePreFilter() { 30 | this.addPreFilter('format=yuv420p'); 31 | } 32 | 33 | /** 34 | * Add zoom and rotate filter parameters 35 | * @public 36 | */ 37 | toSRFilter() { 38 | let filter = super.toSRFilter(); 39 | filter = `format=yuv420p,${filter}`; 40 | filter = filter.replace(/,$/gi, ''); 41 | return filter; 42 | } 43 | 44 | /** 45 | * Get image path 46 | * @public 47 | */ 48 | getPath() { 49 | return this.conf.path || this.conf.image || this.conf.url; 50 | } 51 | 52 | /** 53 | * Get use cache 54 | * @public 55 | */ 56 | getNoCache() { 57 | return this.conf.nocache || this.conf.noCache; 58 | } 59 | 60 | /** 61 | * Add ffmpeg input 62 | * ex: loop 1 -t 20 -i imgs/logo.png 63 | * @private 64 | */ 65 | addInput(command) { 66 | command.addInput(this.getPath()).loop(); 67 | } 68 | 69 | /** 70 | * Reset picture size 71 | * @private 72 | */ 73 | setImageSize(resolve) { 74 | if (this.w) return resolve(); 75 | 76 | const path = this.getPath(); 77 | const noCache = this.getNoCache(); 78 | 79 | if (!noCache) { 80 | if (Cache[path]) { 81 | const info = Cache[path]; 82 | this.setSize(info.width, info.height); 83 | resolve(); 84 | } else { 85 | sizeOf(path, (err, dimensions) => { 86 | if (!err) { 87 | const { width, height } = dimensions; 88 | Cache[path] = { width, height }; 89 | this.setSize(width, height); 90 | } 91 | resolve(); 92 | }); 93 | } 94 | } else { 95 | sizeOf(path, (err, dimensions) => { 96 | const { width, height } = dimensions; 97 | if (!err) this.setSize(width, height); 98 | resolve(); 99 | }); 100 | } 101 | } 102 | 103 | isReady() { 104 | return new Promise(resolve => this.setImageSize(resolve)); 105 | } 106 | } 107 | 108 | module.exports = FFImage; 109 | -------------------------------------------------------------------------------- /lib/node/live.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFLive - live video component-based display component 5 | * 6 | * ####Example: 7 | * 8 | * const live = new FFLive({ path, width: 500, height: 350, loop: true }); 9 | * scene.addChild(live); 10 | * 11 | * 12 | * @class 13 | */ 14 | const FFImage = require('./image'); 15 | 16 | class FFLive extends FFImage { 17 | constructor(conf = { x: 0, y: 0, animations: [] }) { 18 | super({ type: 'live', ...conf }); 19 | } 20 | 21 | /** 22 | * Add live ffmpeg input 23 | * ex: loop 1 -t 20 -i imgs/logo.png 24 | * @private 25 | */ 26 | addInput(command) { 27 | command.addInput(this.getPath()); 28 | } 29 | 30 | isReady() { 31 | return new Promise(resolve => resolve()); 32 | } 33 | } 34 | 35 | module.exports = FFLive; 36 | -------------------------------------------------------------------------------- /lib/node/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFNode Class - FFCreatorLite displays the basic class of the object, 5 | * Other display objects need to inherit from this class. 6 | * 7 | * ####Example: 8 | * 9 | * const node = new FFNode({ x: 10, y: 20 }); 10 | * 11 | * @class 12 | */ 13 | const forEach = require('lodash/forEach'); 14 | const FFBase = require('../core/base'); 15 | const Utils = require('../utils/utils'); 16 | const FilterUtil = require('../utils/filter'); 17 | const FFAnimations = require('../animate/animations'); 18 | 19 | class FFNode extends FFBase { 20 | constructor(conf = {}) { 21 | super({ type: 'node', ...conf }); 22 | 23 | const { x = 0, y = 0, scale = 1, rotate = 0, animations = [], w, h, props } = this.conf; 24 | this.index = 0; 25 | this.fIndex = 0; 26 | this.duration = 0; 27 | this.appearTime = 0; 28 | this.filters = []; 29 | this.preFilters = []; 30 | this.customFilters = []; 31 | this.parent = null; 32 | this.hasInput = false; 33 | 34 | this.setXY(x, y); 35 | this.setWH(w, h); 36 | this.setPorps(props); 37 | this.setScale(scale); 38 | this.setRotate(rotate); 39 | this.animations = new FFAnimations(animations); 40 | this.animations.setTarget(this); 41 | } 42 | 43 | /** 44 | * Get the vid in the ffmpeg filter 45 | * @param {boolean} k - Whether to include outer brackets 46 | * @return {string} vid 47 | * @public 48 | */ 49 | getFId(k = false) { 50 | const vid = `${this.index}:v`; 51 | return k ? `[${vid}]` : `${vid}`; 52 | } 53 | 54 | /** 55 | * Get the input id in the ffmpeg filter 56 | * @param {boolean} k - Whether to include outer brackets 57 | * @return {string} input id 58 | * @public 59 | */ 60 | getInId(k = false) { 61 | if (this.fIndex === 0) { 62 | return this.getFId(k); 63 | } else { 64 | return this.getOutId(k); 65 | } 66 | } 67 | 68 | /** 69 | * Get the output id in the ffmpeg filter 70 | * @param {boolean} k - Whether to include outer brackets 71 | * @return {string} output id 72 | * @public 73 | */ 74 | getOutId(k = false) { 75 | const id = `${this.id}-${this.fIndex}`; 76 | return k ? `[${id}]` : `${id}`; 77 | } 78 | 79 | /** 80 | * Generate new output id 81 | * @public 82 | */ 83 | genNewOutId() { 84 | this.fIndex++; 85 | } 86 | 87 | /** 88 | * Set display object scale 89 | * @param {number} scale 90 | * @public 91 | */ 92 | setScale(scale = 1) { 93 | this.scale = scale; 94 | } 95 | 96 | /** 97 | * Set display object rotate 98 | * @param {number} rotate 99 | * @public 100 | */ 101 | setRotate(rotate = 0) { 102 | this.rotate = rotate; 103 | } 104 | 105 | setAppearTime(appearTime) { 106 | this.appearTime = appearTime; 107 | } 108 | 109 | /** 110 | * Set display object width and height 111 | * @param {number} width - object width 112 | * @param {number} height - object height 113 | * @public 114 | */ 115 | setWH(w, h) { 116 | this.setSize(w, h); 117 | } 118 | 119 | /** 120 | * Set display object width and height 121 | * @param {number} width - object width 122 | * @param {number} height - object height 123 | * @public 124 | */ 125 | setSize(w, h) { 126 | if (w === undefined) return; 127 | this.w = w; 128 | this.h = h; 129 | } 130 | 131 | /** 132 | * Get display object width and height 133 | * @return {string} 1000*120 134 | * @public 135 | */ 136 | getSize(dot = '*') { 137 | return `${this.w}${dot}${this.h}`; 138 | } 139 | 140 | /** 141 | * Set the duration of node in the scene 142 | * @param {number} duration 143 | * @public 144 | */ 145 | setDuration(duration) { 146 | this.duration = duration; 147 | } 148 | 149 | /** 150 | * Set display object x,y position 151 | * @param {number} x - x position 152 | * @param {number} y - y position 153 | * @public 154 | */ 155 | setXY(x = 0, y = 0) { 156 | this.x = x; 157 | this.y = y; 158 | } 159 | 160 | setPorps(a, b) { 161 | if (b === undefined) { 162 | this.props = a; 163 | } else { 164 | this[a] = b; 165 | } 166 | } 167 | 168 | /** 169 | * Set display object x,y position from style object 170 | * @param {object} style - css style object 171 | * @public 172 | */ 173 | setXYFromStyle(style) { 174 | const x = parseInt(style.left); 175 | const y = parseInt(style.top); 176 | return this.setXY(x, y); 177 | } 178 | 179 | /** 180 | * Add one/multiple animations or effects 181 | * @public 182 | */ 183 | setAnimations(animations) { 184 | this.animations.setAnimations(animations); 185 | } 186 | 187 | /** 188 | * Add special animation effects 189 | * @param {string} type - animation effects name 190 | * @param {number} time - time of animation 191 | * @param {number} delay - delay of animation 192 | * @public 193 | */ 194 | addEffect(type, time, delay) { 195 | this.animations.addEffect(type, time, delay); 196 | } 197 | 198 | addAnimate(animation) { 199 | return this.animations.addAnimate(animation); 200 | } 201 | 202 | /** 203 | * concatFilters - Core algorithm: processed into ffmpeg filter syntax 204 | * 1. add preset filters -> pre filter 205 | * 2. scale+rotate -> pre filter 206 | * 3. other filters 207 | * 4. fade/zoompan 208 | * 5. x/y -> last overlay 209 | * 210 | * @param {object} context - context 211 | * @private 212 | */ 213 | concatFilters(context) { 214 | // 1. recorrect position 215 | this.animations.replaceEffectConfVal(); 216 | this.recorrectPosition(); 217 | 218 | // 2. add preset filters 219 | this.filters = this.preFilters.concat(this.filters); 220 | 221 | // 3. add scale rotate filters 222 | const srFilter = FilterUtil.assembleSRFilter({ scale: this.scale, rotate: this.rotate }); 223 | if (srFilter) this.filters.push(srFilter); 224 | 225 | // 4. add others custom filters 226 | this.filters = this.filters.concat(this.customFilters); 227 | 228 | // 5. add animations filters 229 | this.appearTime = this.appearTime || this.animations.getAppearTime(); 230 | // Because overlay enable is used, remove this 231 | // this.animations.modifyDelayTime(this.appearTime); 232 | const aniFilters = this.animations.concatFilters(); 233 | this.filters = this.filters.concat(aniFilters); 234 | 235 | // 6. set overlay filter x/y 236 | if (!FilterUtil.getOverlayFromFilters(this.filters)) { 237 | const xyFilter = FilterUtil.createOverlayFilter(this.x, this.y); 238 | this.filters.push(xyFilter); 239 | } 240 | 241 | // 7. add appearTime setpts 242 | // Because overlay enable is used, remove this 243 | // this.filters.push(FilterUtil.createSetptsFilter(this.appearTime)); 244 | 245 | // 8. add this duration time 246 | this.addDurationToOverlay(); 247 | 248 | // 9. add inputs and outputs 249 | this.addInputsAndOutputs(context); 250 | return this.filters; 251 | } 252 | 253 | recorrectPosition() { 254 | if (this.animations.hasAnimate('rotate')) { 255 | const w = this.w; 256 | const h = this.h; 257 | const diagonal = Math.sqrt(w * w + h * h); 258 | this.x += Utils.floor((w - diagonal) / 2, 0); 259 | this.y += Utils.floor((h - diagonal) / 2, 0); 260 | } else if (this.animations.hasZoompanPad()) { 261 | //const scale = this.animations.getMaxScale(); 262 | // this.x -= ((scale - 1) * this.w) / 2; 263 | // this.y -= ((scale - 1) * this.h) / 2; 264 | } 265 | } 266 | 267 | /** 268 | * Add Duration interval time to filter 269 | * @private 270 | */ 271 | addDurationToOverlay() { 272 | this.appearTime = this.appearTime || this.animations.getAppearTime(); 273 | this.duration = this.duration || this.animations.getDuration(); 274 | 275 | FilterUtil.addDurationToOverlay({ 276 | filters: this.filters, 277 | appearTime: this.appearTime, 278 | duration: this.duration, 279 | }); 280 | } 281 | 282 | /** 283 | * Add input param and output param to filter 284 | * @private 285 | */ 286 | addInputsAndOutputs(context) { 287 | if (!this.filters.length) return; 288 | 289 | forEach(this.filters, (filter, index) => { 290 | const inputs = this.getInId(); 291 | this.genNewOutId(); 292 | const outputs = this.getOutId(); 293 | 294 | this.filters[index] = FilterUtil.setInputsAndOutputs({ 295 | filter, 296 | inputs, 297 | outputs, 298 | contextInputs: context.input, 299 | }); 300 | }); 301 | 302 | // 5. set context input 303 | context.input = this.getOutId(); 304 | } 305 | 306 | /** 307 | * other methods 308 | * @private 309 | */ 310 | addFilter(filter) { 311 | this.customFilters.push(filter); 312 | } 313 | 314 | addPreFilter(filter) { 315 | this.preFilters.push(filter); 316 | } 317 | 318 | addInput(command) { 319 | //command.addInput(this.conf.path); 320 | } 321 | 322 | addOutput(command) { 323 | //command.addInput(this.conf.path); 324 | } 325 | 326 | addOptions() {} 327 | 328 | addBlend(blend) { 329 | this.addPreFilter(`blend=all_expr='${blend}'`); 330 | } 331 | 332 | addTBlend(blend, mode = 'all_mode') { 333 | this.addPreFilter(`tblend=${mode}=${blend}`); 334 | } 335 | 336 | isReady() { 337 | return new Promise(resolve => resolve()); 338 | } 339 | 340 | toFilter() {} 341 | } 342 | 343 | module.exports = FFNode; 344 | -------------------------------------------------------------------------------- /lib/node/scene.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFScene - Scene component, a container used to load display object components. 5 | * 6 | * ####Example: 7 | * 8 | * const scene = new FFScene(); 9 | * scene.setBgColor("#ffcc00"); 10 | * scene.setDuration(6); 11 | * creator.addChild(scene); 12 | * 13 | * @class 14 | */ 15 | const path = require('path'); 16 | const rmfr = require('rmfr'); 17 | const fs = require('fs-extra'); 18 | const FFCon = require('./cons'); 19 | const FFAudio = require('./audio'); 20 | const FFImage = require('./image'); 21 | const Utils = require('../utils/utils'); 22 | const forEach = require('lodash/forEach'); 23 | const FFContext = require('../core/context'); 24 | const FFmpegUtil = require('../utils/ffmpeg'); 25 | const FFBackGround = require('../node/background'); 26 | const FFTransition = require('../animate/transition'); 27 | 28 | class FFScene extends FFCon { 29 | constructor(conf) { 30 | super({ type: 'scene', ...conf }); 31 | 32 | this.percent = 0; 33 | this.duration = 10; 34 | this.directory = ''; 35 | this.context = null; 36 | this.command = null; 37 | this.transition = null; 38 | this.pathId = Utils.uid(); 39 | 40 | this.setBgColor('#000000'); 41 | this.addBlankImage(); 42 | } 43 | 44 | addBlankImage() { 45 | const blank = path.join(__dirname, '../assets/blank.png'); 46 | this.addChild(new FFImage({ path: blank, x: -10, y: 0 })); 47 | } 48 | 49 | /** 50 | * Set the time the scene stays in the scree 51 | * @param {number} duration - the time the scene 52 | * @public 53 | */ 54 | setDuration(duration) { 55 | this.background && this.background.setDuration(duration); 56 | this.duration = duration; 57 | } 58 | 59 | /** 60 | * Set scene transition animation 61 | * @param {string|object} name - transition animation name or animation conf object 62 | * @param {number} duration - transition animation duration 63 | * @param {object} params - transition animation params 64 | * @public 65 | */ 66 | setTransition(name, duration) { 67 | if (typeof name === 'object') { 68 | name = name.name; 69 | duration = name.duration; 70 | } 71 | this.transition = new FFTransition({ name, duration }); 72 | } 73 | 74 | /** 75 | * Set background color 76 | * @param {string} bgcolor - background color 77 | * @public 78 | */ 79 | setBgColor(color) { 80 | if (this.background) this.removeChild(this.background); 81 | this.background = new FFBackGround({ color }); 82 | this.addChildAt(this.background, 0); 83 | } 84 | 85 | getFile() { 86 | return this.filepath; 87 | } 88 | 89 | getNormalDuration() { 90 | const transTime = this.transition ? this.transition.duration : 0; 91 | return this.duration - transTime; 92 | } 93 | 94 | createFilepath() { 95 | const { id, pathId } = this; 96 | const cacheDir = this.rootConf('cacheDir').replace(/\/$/, ''); 97 | const format = this.rootConf('cacheFormat'); 98 | const dir = `${cacheDir}/${pathId}`; 99 | const directory = dir.replace(/\/$/, ''); 100 | const filepath = `${directory}/${id}.${format}`; 101 | fs.ensureDir(dir); 102 | 103 | this.filepath = filepath; 104 | this.directory = directory; 105 | return filepath; 106 | } 107 | 108 | // about command 109 | createNewCommand() { 110 | const conf = this.rootConf(); 111 | const threads = conf.getVal('threads'); 112 | const upStreaming = conf.getVal('upStreaming'); 113 | const command = FFmpegUtil.createCommand({ threads }); 114 | if (!upStreaming) { 115 | command.setDuration(this.duration); 116 | } 117 | 118 | this.command = command; 119 | this.context = new FFContext(); 120 | } 121 | 122 | toCommand() { 123 | const command = this.command; 124 | this.addNodesInputCommand(command); 125 | this.toFiltersCommand(command); 126 | this.addCommandOptions(command); 127 | this.addCommandOutputs(command); 128 | this.addNodesOutputCommand(command); 129 | this.addCommandEvents(command); 130 | 131 | return command; 132 | } 133 | 134 | start() { 135 | this.createNewCommand(); 136 | this.toCommand(); 137 | this.command.run(); 138 | } 139 | 140 | addCommandOptions(command) { 141 | const conf = this.rootConf(); 142 | const upStreaming = conf.getVal('upStreaming'); 143 | 144 | if (upStreaming) { 145 | FFmpegUtil.addDefaultOptions({ command, conf, audio: true }); 146 | } else { 147 | FFmpegUtil.addDefaultOptions({ command, conf, audio: false }); 148 | } 149 | 150 | const fps = conf.getVal('fps'); 151 | if (fps != 25) command.outputFPS(fps); 152 | 153 | const { children } = this; 154 | forEach(children, child => child.addOptions(command)); 155 | } 156 | 157 | addCommandOutputs(command) { 158 | let filepath; 159 | const conf = this.rootConf(); 160 | const output = conf.getVal('output'); 161 | const upStreaming = conf.getVal('upStreaming'); 162 | 163 | if (upStreaming) { 164 | filepath = output; 165 | command.outputOptions(['-f', 'flv']); 166 | } else { 167 | filepath = this.createFilepath(); 168 | } 169 | 170 | command.output(filepath); 171 | } 172 | 173 | deleteCacheFile() { 174 | const conf = this.rootConf(); 175 | const debug = conf.getVal('debug'); 176 | if (!debug && this.directory) rmfr(this.directory); 177 | } 178 | 179 | // addInputs 180 | addNodesInputCommand(command) { 181 | forEach(this.children, child => child.addInput(command)); 182 | } 183 | 184 | // addOutputs 185 | addNodesOutputCommand(command) { 186 | forEach(this.children, child => child.addOutput(command)); 187 | } 188 | 189 | // filters toCommand 190 | toFiltersCommand(command) { 191 | const filters = this.concatFilters(); 192 | command.complexFilter(filters, this.context.input); 193 | } 194 | 195 | toTransFilter(offset) { 196 | return this.transition.toFilter(offset); 197 | } 198 | 199 | fillTransition() { 200 | if (!this.transition) { 201 | this.setTransition('fade', 0.5); 202 | } 203 | } 204 | 205 | /** 206 | * Combine various filters as ffmpeg parameters 207 | * @private 208 | */ 209 | concatFilters() { 210 | forEach(this.children, child => { 211 | const filters = child.concatFilters(this.context); 212 | if (filters.length) this.filters = this.filters.concat(filters); 213 | }); 214 | 215 | return this.filters; 216 | } 217 | 218 | getTotalFrames() { 219 | return this.rootConf().getVal('fps') * this.duration; 220 | } 221 | 222 | // add command eventemitter3 223 | addCommandEvents(command) { 224 | const log = this.rootConf('log'); 225 | FFmpegUtil.addCommandEvents({ 226 | log, 227 | command, 228 | type: 'single', 229 | totalFrames: this.getTotalFrames(), 230 | start: this.commandEventHandler.bind(this), 231 | error: this.commandEventHandler.bind(this), 232 | complete: this.commandEventHandler.bind(this), 233 | progress: this.commandEventHandler.bind(this), 234 | }); 235 | } 236 | 237 | commandEventHandler(event) { 238 | event.target = this; 239 | this.emits(event); 240 | } 241 | 242 | addAudio() { 243 | const conf = this.rootConf(); 244 | const audio = conf.getVal('audio'); 245 | const loop = conf.getVal('audioLoop'); 246 | if (audio) this.addChild(new FFAudio({ audio, loop })); 247 | } 248 | 249 | isReady() { 250 | return new Promise(resolve => { 251 | let readyIndex = 0; 252 | forEach(this.children, child => { 253 | child.isReady().then(() => { 254 | readyIndex++; 255 | if (readyIndex >= this.children.length) { 256 | resolve(); 257 | } 258 | }); 259 | }); 260 | }); 261 | } 262 | 263 | destroy() { 264 | FFmpegUtil.destroy(this.command); 265 | super.destroy(); 266 | this.transition.destroy(); 267 | this.transition = null; 268 | this.context = null; 269 | this.command = null; 270 | } 271 | } 272 | 273 | module.exports = FFScene; 274 | -------------------------------------------------------------------------------- /lib/node/text.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFText - Text component-based display component 5 | * 6 | * ####Example: 7 | * 8 | * const text = new FFText({ text: "hello world", x: 400, y: 300 }); 9 | * text.setColor("#ffffff"); 10 | * text.setBackgroundColor("#000000"); 11 | * text.addEffect("fadeIn", 1, 1); 12 | * scene.addChild(text); 13 | * 14 | * ####Note: 15 | * fontfile - The font file to be used for drawing text. 16 | * The path must be included. This parameter is mandatory if the fontconfig support is disabled. 17 | * 18 | * @class 19 | */ 20 | const forEach = require('lodash/forEach'); 21 | const FFNode = require('./node'); 22 | const Utils = require('../utils/utils'); 23 | const FilterUtil = require('../utils/filter'); 24 | 25 | class FFText extends FFNode { 26 | constructor(conf = { x: 0, y: 0, animations: [] }) { 27 | super({ type: 'text', ...conf }); 28 | 29 | const { 30 | color = 'black', 31 | backgroundColor, 32 | fontSize = 24, 33 | text = '', 34 | font, 35 | fontfile, 36 | fontFamily, 37 | } = conf; 38 | 39 | this.text = text; 40 | this.fontcolor = color; 41 | this.fontsize = fontSize; 42 | this.boxcolor = backgroundColor; 43 | this.fontfile = font || fontFamily || fontfile; 44 | } 45 | 46 | /** 47 | * Set text value 48 | * @param {string} text - text value 49 | * @public 50 | */ 51 | setText(text) { 52 | this.text = text; 53 | } 54 | 55 | /** 56 | * Set background color 57 | * @param {string} backgroundColor - the background color value 58 | * @public 59 | */ 60 | setBackgroundColor(backgroundColor) { 61 | this.boxcolor = backgroundColor; 62 | } 63 | 64 | /** 65 | * Set text color value 66 | * @param {string} color - the text color value 67 | * @public 68 | */ 69 | setColor(color) { 70 | this.fontcolor = color; 71 | } 72 | 73 | /** 74 | * Set text font file path 75 | * @param {string} file - text font file path 76 | * @public 77 | */ 78 | setFontFile(file) { 79 | this.fontfile = file; 80 | } 81 | 82 | /** 83 | * Set text font file path 84 | * @param {string} file - text font file path 85 | * @public 86 | */ 87 | setFont(file) { 88 | return this.setFontFile(file); 89 | } 90 | 91 | /** 92 | * Set text style by object 93 | * @param {object} style - style by object 94 | * @public 95 | */ 96 | setStyle(style) { 97 | if (style.color) this.fontcolor = style.color; 98 | if (style.opacity) this.alpha = style.opacity; 99 | if (style.border) this.borderw = style.border; 100 | if (style.borderSize) this.borderw = style.borderSize; 101 | if (style.fontSize) this.fontsize = parseInt(style.fontSize); 102 | if (style.borderColor) this.bordercolor = style.borderColor; 103 | if (style.backgroundColor) this.boxcolor = style.backgroundColor; 104 | if (style.lineSpacing) this.line_spacing = parseInt(style.lineSpacing); 105 | } 106 | 107 | /** 108 | * Set text border value 109 | * @param {number} borderSize - style border width size 110 | * @param {string} borderColor - style border color 111 | * @public 112 | */ 113 | setBorder(borderSize, borderColor) { 114 | this.borderw = borderSize; 115 | this.bordercolor = borderColor; 116 | } 117 | 118 | /** 119 | * concatFilters - Core algorithm: processed into ffmpeg filter syntax 120 | * @param {object} context - context 121 | * @private 122 | */ 123 | concatFilters(context) { 124 | this.animations.replaceEffectConfVal(); 125 | 126 | this.filters = this.preFilters.concat(this.filters); 127 | this.filters = this.filters.concat(this.customFilters); 128 | const aniFilters = this.animations.concatFilters(); 129 | this.resetXYByAnimations(aniFilters); 130 | this.resetAlphaByAnimations(aniFilters); 131 | 132 | const filter = this.toFilter(); 133 | if (filter) { 134 | this.filters.push(filter); 135 | this.addInputsAndOutputs(context); 136 | } 137 | 138 | return this.filters; 139 | } 140 | 141 | resetXYByAnimations(filters) { 142 | const { x, y } = this.getXYFromOverlay(filters); 143 | this.x = x; 144 | this.y = y; 145 | } 146 | 147 | resetAlphaByAnimations(filters) { 148 | const alpha = this.getAlphaFromFilters(filters); 149 | this.alpha = alpha; 150 | } 151 | 152 | getAlphaFromFilters(filters) { 153 | let alpha; 154 | forEach(filters, f => { 155 | if (f.filter == 'alpha') { 156 | alpha = f.options.alpha; 157 | } 158 | }); 159 | return alpha; 160 | } 161 | 162 | getXYFromOverlay(filters) { 163 | let xy = { x: this.x, y: this.y }; 164 | forEach(filters, filter => { 165 | if (filter.filter == 'overlay') { 166 | xy = { x: filter.options.x, y: filter.options.y }; 167 | } 168 | }); 169 | return xy; 170 | } 171 | 172 | /** 173 | * Converted to ffmpeg filter command line parameters 174 | * @private 175 | */ 176 | toFilter() { 177 | // Usually FFMpeg text must specify the font file directory 178 | // if (!this.fontfile) { 179 | // console.error('[FFCreatorLite] Sorry FFText no input font file!'); 180 | // return; 181 | // } 182 | 183 | const appearTime = this.appearTime || this.animations.getAppearTime(); 184 | const duration = this.duration || this.animations.getDuration(); 185 | const enable = FilterUtil.createFilterEnable({ appearTime, duration }); 186 | 187 | const options = { 188 | line_spacing: this.line_spacing, 189 | bordercolor: this.bordercolor, 190 | borderw: this.borderw, 191 | fontcolor: this.fontcolor, 192 | fontfile: this.fontfile, 193 | fontsize: this.fontsize, 194 | boxcolor: this.boxcolor, 195 | text: this.text, 196 | alpha: this.alpha, 197 | x: this.x, 198 | y: this.y, 199 | enable, 200 | }; 201 | 202 | Utils.deleteUndefined(options); 203 | if (options.boxcolor) options.box = 1; 204 | 205 | return { filter: 'drawtext', options }; 206 | } 207 | 208 | /** 209 | * Add input param and output param to filter 210 | * @private 211 | */ 212 | addInputsAndOutputs(context) { 213 | if (!this.filters.length) return; 214 | 215 | forEach(this.filters, (filter, index) => { 216 | let inputs = index == 0 ? context.input : this.getInId(); 217 | this.genNewOutId(); 218 | let outputs = this.getOutId(); 219 | 220 | this.filters[index] = FilterUtil.setInputsAndOutputs({ 221 | filter, 222 | inputs, 223 | outputs, 224 | contextInputs: context.input, 225 | }); 226 | }); 227 | 228 | // 5. set context input 229 | context.input = this.getOutId(); 230 | } 231 | } 232 | 233 | module.exports = FFText; 234 | -------------------------------------------------------------------------------- /lib/node/video.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFVideo - Video component-based display component 5 | * 6 | * ####Example: 7 | * 8 | * const video = new FFVideo({ path, width: 500, height: 350, loop: true }); 9 | * scene.addChild(video); 10 | * 11 | * 12 | * @class 13 | */ 14 | const isNumber = require('lodash/isNumber'); 15 | const FFImage = require('./image'); 16 | 17 | class FFVideo extends FFImage { 18 | constructor(conf = { x: 0, y: 0, animations: [] }) { 19 | super({ type: 'video', ...conf }); 20 | } 21 | 22 | /** 23 | * Add video ffmpeg input 24 | * ex: loop 1 -t 20 -i imgs/logo.png 25 | * @private 26 | */ 27 | addInput(command) { 28 | const { loop, delay } = this.conf; 29 | 30 | if (loop) { 31 | const num = isNumber(loop) ? isNumber(loop) : -1; 32 | command.addInput(this.getPath()).inputOption('-stream_loop', `${num}`); 33 | } else { 34 | command.addInput(this.getPath()); 35 | } 36 | if (delay) command.inputOption('-itsoffset', delay); 37 | } 38 | 39 | addOutput(command) { 40 | if (this.conf.audio) { 41 | command.outputOptions(["-map", `${this.index}:a`]); 42 | } 43 | } 44 | 45 | setLoop(loop) { 46 | this.conf.loop = loop; 47 | } 48 | 49 | setDelay(delay) { 50 | this.conf.delay = delay; 51 | } 52 | 53 | isReady() { 54 | return new Promise(resolve => resolve()); 55 | } 56 | } 57 | 58 | module.exports = FFVideo; 59 | -------------------------------------------------------------------------------- /lib/utils/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Cache 5 | * @object 6 | */ 7 | const Cache = {}; 8 | 9 | module.exports = Cache; 10 | -------------------------------------------------------------------------------- /lib/utils/date.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * DateUtil - Auxiliary utils functions for date. 5 | * 6 | * ####Example: 7 | * 8 | * const time1 = DateUtil.toMilliseconds(.5); 9 | * const time2 = DateUtil.hmsToSeconds("00:03:21"); 10 | * 11 | * @object 12 | */ 13 | const DateUtil = { 14 | /** 15 | * Convert seconds to hh:mm:ss format 16 | * @param {number} sec - second 17 | * @return {string} hh:mm:ss result 18 | * @public 19 | */ 20 | secondsToHms(sec) { 21 | let hours = Math.floor(sec / 3600); 22 | let minutes = Math.floor((sec - hours * 3600) / 60); 23 | let seconds = sec - hours * 3600 - minutes * 60; 24 | 25 | if (hours < 10) { 26 | hours = '0' + hours; 27 | } 28 | 29 | if (minutes < 10) { 30 | minutes = '0' + minutes; 31 | } 32 | 33 | if (seconds < 10) { 34 | seconds = '0' + seconds; 35 | } 36 | 37 | return hours + ':' + minutes + ':' + seconds; 38 | }, 39 | 40 | /** 41 | * Convert hh:mm:ss format time to seconds 42 | * @param {string} hms - hh:mm:ss format time 43 | * @return {number} seconds second 44 | * @public 45 | */ 46 | hmsToSeconds(hms) { 47 | const a = hms.split(':'); 48 | const seconds = +a[0] * 60 * 60 + +a[1] * 60 + +a[2]; 49 | return seconds; 50 | }, 51 | 52 | /** 53 | * Convert time to millisecond format 54 | * @param {number} time - second time 55 | * @return {string} millisecondt 56 | * @public 57 | */ 58 | toMilliseconds(time) { 59 | if (time === 0) return 0; 60 | return time < 100 ? time * 1000 : time; 61 | }, 62 | 63 | /** 64 | * Convert time to second format 65 | * @param {number} time - millisecondt time 66 | * @return {string} second 67 | * @public 68 | */ 69 | toSeconds(time) { 70 | if (time === 0) return 0; 71 | return time > 100 ? time / 1000 : time; 72 | }, 73 | }; 74 | 75 | module.exports = DateUtil; 76 | -------------------------------------------------------------------------------- /lib/utils/ffmpeg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FFmpegUtil - Utility function collection of ffmpeg 5 | * 6 | * ####Example: 7 | * 8 | * FFmpegUtil.addDefaultOptions({ 9 | * command: this.mainCommand, 10 | * audio: this.getConf('audio'), 11 | * }); 12 | * 13 | * @object 14 | */ 15 | const isArray = require('lodash/isArray'); 16 | const forEach = require('lodash/forEach'); 17 | const ffmpeg = require('fluent-ffmpeg'); 18 | 19 | const FFmpegUtil = { 20 | getFFmpeg() { 21 | return ffmpeg; 22 | }, 23 | 24 | setFFmpegPath(path) { 25 | ffmpeg.setFfmpegPath(path); 26 | }, 27 | 28 | setFFprobePath(path) { 29 | ffmpeg.setFfprobePath(path); 30 | }, 31 | 32 | createCommand(conf = {}) { 33 | const { threads = 1 } = conf; 34 | const command = ffmpeg(); 35 | if (threads > 1) command.addOptions([`-threads ${threads}`]); 36 | return command; 37 | }, 38 | 39 | concatOpts(opts, arr) { 40 | if (isArray(arr)) { 41 | forEach(arr, o => opts.push(o)); 42 | } else { 43 | opts.push(arr); 44 | } 45 | }, 46 | 47 | /** 48 | * get Current ffmpeg version 49 | * @public 50 | */ 51 | getVersion() { 52 | return new Promise(resolve => { 53 | ffmpeg() 54 | .addOptions([`-version`]) 55 | .output('./') 56 | .on('end', (result = '') => { 57 | result = result.replace(/copyright[\s\S]*/gi, ''); 58 | let version = result.split(' ')[2]; 59 | version = version.split('.').join(''); 60 | resolve(parseInt(version)); 61 | }) 62 | .on('error', () => { 63 | let version = '4.2.2'; 64 | version = version.split('.').join(''); 65 | resolve(parseInt(version)); 66 | }) 67 | .run(); 68 | }); 69 | }, 70 | 71 | /** 72 | * Add some basic output properties 73 | * @public 74 | */ 75 | addDefaultOptions({ command, conf, audio }) { 76 | const vb = conf.getVal('vb'); 77 | const crf = conf.getVal('crf'); 78 | const preset = conf.getVal('preset'); 79 | const vprofile = conf.getVal('vprofile'); 80 | const upStreaming = conf.getVal('upStreaming'); 81 | 82 | let outputOptions = [] 83 | //---- misc ---- 84 | .concat([ 85 | // '-map', 86 | // '0', 87 | '-hide_banner', // hide_banner - parameter, you can display only meta information 88 | '-map_metadata', 89 | '-1', 90 | '-map_chapters', 91 | '-1', 92 | ]) 93 | 94 | //---- video ---- 95 | .concat([ 96 | '-c:v', 97 | 'libx264', // c:v - H.264 98 | '-profile:v', 99 | vprofile, // profile:v - main profile: mainstream image quality. Provide I / P / B frames, default 100 | '-preset', 101 | preset, // preset - compromised encoding speed 102 | '-crf', 103 | crf, // crf - The range of quantization ratio is 0 ~ 51, where 0 is lossless mode, 23 is the default value, 51 may be the worst 104 | '-movflags', 105 | 'faststart', 106 | '-pix_fmt', 107 | 'yuv420p', 108 | ]); 109 | 110 | //---- vb ----- 111 | if (vb) { 112 | outputOptions = outputOptions.concat(['-vb', vb]); 113 | } 114 | 115 | //---- audio ---- 116 | if (audio) { 117 | outputOptions = outputOptions.concat(['-c:a', 'copy', '-shortest']); 118 | } 119 | 120 | //---- live stream ---- 121 | if (!upStreaming) { 122 | outputOptions = outputOptions.concat(['-map', '0']); 123 | } 124 | 125 | command.outputOptions(outputOptions); 126 | return command; 127 | }, 128 | 129 | /** 130 | * Add event to ffmpeg command 131 | * @public 132 | */ 133 | addCommandEvents({ 134 | command, 135 | log = false, 136 | start, 137 | complete, 138 | error, 139 | progress, 140 | totalFrames, 141 | type = '', 142 | }) { 143 | command 144 | .on('start', commandLine => { 145 | if (log) console.log(`${type}: ${commandLine}`); 146 | const event = { type: `${type}-start`, command: commandLine }; 147 | start && start(event); 148 | }) 149 | .on('progress', function(p) { 150 | const frames = p.frames; 151 | const fpercent = frames / totalFrames; 152 | const event = { type: `${type}-progress`, fpercent, frames, totalFrames }; 153 | progress && progress(event); 154 | }) 155 | .on('end', () => { 156 | const event = { type: `${type}-complete` }; 157 | complete && complete(event); 158 | }) 159 | .on('error', err => { 160 | const event = { type: `${type}-error`, error: err }; 161 | console.log('=============='); 162 | console.log(err); 163 | error && error(event); 164 | }); 165 | 166 | return command; 167 | }, 168 | 169 | destroy(command) { 170 | try { 171 | command.removeAllListeners(); 172 | command.kill(); 173 | command._inputs.length = 0; 174 | command._currentInput = null; 175 | } catch (e) {} 176 | }, 177 | }; 178 | 179 | module.exports = FFmpegUtil; 180 | -------------------------------------------------------------------------------- /lib/utils/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * FilterUtil - Utility function collection of filter 5 | * 6 | * ####Example: 7 | * 8 | * const padFilter = FilterUtil.createPadFilter(maxScale); 9 | * const scaleFilter = FilterUtil.createScaleFilter(4000); 10 | * const zoomFilter = FilterUtil.createZoomFilter({x, y, z, s: size, fps, d: frames}); 11 | * 12 | * @object 13 | */ 14 | 15 | const Utils = require('./utils'); 16 | const isArray = require('lodash/isArray'); 17 | 18 | const FilterUtil = { 19 | // if include overlay filter 20 | getOverlayFromFilters(filters) { 21 | for (let i = 0; i < filters.length; i++) { 22 | const filter = filters[i]; 23 | if (typeof filter === 'string') { 24 | if (/^overlay=/g.test(filter)) return filter; 25 | } else { 26 | if (filter.filter == 'overlay') return filter; 27 | } 28 | } 29 | 30 | return null; 31 | }, 32 | 33 | createPadFilter(scale) { 34 | return `format=yuva420p,pad=${scale}*iw:${scale}*ih:(ow-iw)/2:(oh-ih)/2:black@0`; 35 | }, 36 | 37 | createScaleFilter(w = 6000) { 38 | return `scale=${w}x${w}`; 39 | }, 40 | 41 | createZoomFilter({ x, y, z, s, fps, d }) { 42 | let filter = 'zoompan='; 43 | if (z) filter += `z='${z}'`; 44 | if (d) filter += `:d='${d}'`; 45 | if (x) filter += `:x='${x}'`; 46 | if (y) filter += `:y='${y}'`; 47 | if (s) filter += `:s='${s}'`; 48 | if (fps) filter += `:fps='${fps}'`; 49 | 50 | return filter; 51 | }, 52 | 53 | createOverlayFilter(x, y) { 54 | return { 55 | filter: 'overlay', 56 | options: { x, y }, 57 | }; 58 | }, 59 | 60 | createSetptsFilter(appearTime = 0) { 61 | return `setpts=PTS-STARTPTS+${appearTime}/TB`; 62 | }, 63 | 64 | // assemble scale rotate filters 65 | assembleSRFilter({ scale, rotate }) { 66 | let filter = ''; 67 | if (scale != 1) { 68 | scale = Utils.floor(scale, 2); 69 | filter += `scale=iw*${scale}:ih*${scale},`; 70 | } 71 | 72 | if (rotate != 0) { 73 | rotate = Utils.angleToPI(this.rotate); 74 | filter += `rotate=${rotate},`; 75 | } 76 | 77 | filter = filter.replace(/,$/gi, ''); 78 | return filter; 79 | }, 80 | 81 | // set inputs and outputs 82 | setInputsAndOutputs({ filter, contextInputs, inputs, outputs }) { 83 | if (typeof filter === 'string') { 84 | // is overlay filter 85 | if (/^overlay=/g.test(filter)) { 86 | filter = `[${contextInputs}][${inputs}]${filter}[${outputs}]`; 87 | } else { 88 | filter = `[${inputs}]${filter}[${outputs}]`; 89 | } 90 | } else { 91 | // is overlay filter 92 | if (filter.filter === 'overlay') { 93 | filter.inputs = [contextInputs, inputs]; 94 | filter.outputs = outputs; 95 | } else { 96 | filter.inputs = inputs; 97 | filter.outputs = outputs; 98 | } 99 | } 100 | 101 | return filter; 102 | }, 103 | 104 | // add overlay filter duration --- 105 | addDurationToOverlay({ filters, appearTime, duration }) { 106 | let overlay = this.getOverlayFromFilters(filters); 107 | if (!overlay) return; 108 | if (appearTime <= 0) return; 109 | 110 | const index = filters.indexOf(overlay); 111 | const enable = this.createFilterEnable({ appearTime, duration }); 112 | if (typeof overlay === 'string') { 113 | overlay += `:enable='${enable}'`; 114 | } else { 115 | overlay.options.enable = enable; 116 | } 117 | filters[index] = overlay; 118 | }, 119 | 120 | createFilterEnable({ appearTime, duration }) { 121 | if (duration <= 0) duration = 99999; 122 | return `between(t,${appearTime},${duration})`; 123 | }, 124 | 125 | makeFilterStrings(filters) { 126 | const streamRegexp = /^\[?(.*?)\]?$/; 127 | const filterEscapeRegexp = /[,]/; 128 | 129 | return filters.map(function(filterSpec) { 130 | if (typeof filterSpec === 'string') return filterSpec; 131 | 132 | let filterString = ''; 133 | if (isArray(filterSpec.inputs)) { 134 | filterString += filterSpec.inputs 135 | .map(streamSpec => streamSpec.replace(streamRegexp, '[$1]')) 136 | .join(''); 137 | } else if (typeof filterSpec.inputs === 'string') { 138 | filterString += filterSpec.inputs.replace(streamRegexp, '[$1]'); 139 | } 140 | 141 | // Add filter 142 | filterString += filterSpec.filter; 143 | 144 | // Add options 145 | if (filterSpec.options) { 146 | if (typeof filterSpec.options === 'string' || typeof filterSpec.options === 'number') { 147 | // Option string 148 | filterString += '=' + filterSpec.options; 149 | } else if (isArray(filterSpec.options)) { 150 | // Option array (unnamed options) 151 | filterString += 152 | '=' + 153 | filterSpec.options 154 | .map(function(option) { 155 | if (typeof option === 'string' && option.match(filterEscapeRegexp)) { 156 | return "'" + option + "'"; 157 | } else { 158 | return option; 159 | } 160 | }) 161 | .join(':'); 162 | } else if (Object.keys(filterSpec.options).length) { 163 | // Option object (named options) 164 | filterString += 165 | '=' + 166 | Object.keys(filterSpec.options) 167 | .map(function(option) { 168 | let value = filterSpec.options[option]; 169 | 170 | if (typeof value === 'string' && value.match(filterEscapeRegexp)) { 171 | value = "'" + value + "'"; 172 | } 173 | 174 | return option + '=' + value; 175 | }) 176 | .join(':'); 177 | } 178 | } 179 | 180 | // Add outputs 181 | if (isArray(filterSpec.outputs)) { 182 | filterString += filterSpec.outputs 183 | .map(streamSpec => streamSpec.replace(streamRegexp, '[$1]')) 184 | .join(''); 185 | } else if (typeof filterSpec.outputs === 'string') { 186 | filterString += filterSpec.outputs.replace(streamRegexp, '[$1]'); 187 | } 188 | 189 | return filterString; 190 | }); 191 | }, 192 | }; 193 | 194 | module.exports = FilterUtil; 195 | -------------------------------------------------------------------------------- /lib/utils/perf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Perf - Simple management performance statistics. Used to count cpu and memory usage. 5 | * 6 | * ####Example: 7 | * 8 | * Perf.start(); 9 | * ... 10 | * Perf.end(); 11 | * 12 | * @object 13 | */ 14 | const os = require('os'); 15 | 16 | const Perf = { 17 | old: 0, 18 | now: 0, 19 | id: 0, 20 | t: 0, 21 | stats1: null, 22 | stats2: null, 23 | enabled: true, 24 | 25 | setEnabled(enabled) { 26 | this.enabled = enabled; 27 | }, 28 | 29 | /** 30 | * Statistics start 31 | * @public 32 | */ 33 | start() { 34 | this.old = Date.now(); 35 | this.stats1 = this.getCpuInfo(); 36 | }, 37 | 38 | /** 39 | * Statistics end 40 | * @public 41 | */ 42 | end() { 43 | this.now = Date.now(); 44 | this.t = this.now - this.old; 45 | if (this.enabled) this.analysis(); 46 | this.old = Date.now(); 47 | }, 48 | 49 | analysis() { 50 | console.log('------------------------------------------------------'); 51 | console.log('Perf', this.id++, this.t, this.getMemoryUsage(), this.getCpuUsage()); 52 | console.log('------------------------------------------------------'); 53 | }, 54 | 55 | getCpuUsage() { 56 | this.stats2 = this.getCpuInfo(); 57 | const startIdle = this.stats1.idle; 58 | const startTotal = this.stats1.total; 59 | const endIdle = this.stats2.idle; 60 | const endTotal = this.stats2.total; 61 | 62 | const idle = endIdle - startIdle; 63 | const total = endTotal - startTotal; 64 | const perc = idle / total; 65 | 66 | const useage = 1 - perc; 67 | return this.getPercent(useage, 1); 68 | }, 69 | 70 | getInfo() { 71 | return `time ${this.t} memory ${this.getMemoryUsage()} cpu ${this.getCpuUsage()}`; 72 | }, 73 | 74 | getMemoryUsage() { 75 | const useage = process.memoryUsage(); 76 | const heapTotal = useage.heapTotal; 77 | const heapUsed = useage.heapUsed; 78 | const proportion = this.getPercent(heapUsed, heapTotal); 79 | return `${heapUsed} ${heapTotal} ${proportion} ${useage.rss}`; 80 | }, 81 | 82 | getCpuInfo() { 83 | const cpus = os.cpus(); 84 | 85 | let user = 0; 86 | let nice = 0; 87 | let sys = 0; 88 | let idle = 0; 89 | let irq = 0; 90 | let total = 0; 91 | 92 | for (let cpu in cpus) { 93 | if (!Object.prototype.hasOwnProperty.call(cpus, cpu)) continue; 94 | user += cpus[cpu].times.user; 95 | nice += cpus[cpu].times.nice; 96 | sys += cpus[cpu].times.sys; 97 | irq += cpus[cpu].times.irq; 98 | idle += cpus[cpu].times.idle; 99 | } 100 | 101 | total = user + nice + sys + idle + irq; 102 | return { idle, total }; 103 | }, 104 | 105 | getPercent(a, b) { 106 | return Math.floor((a * 1000) / b) / 10 + '%'; 107 | }, 108 | }; 109 | 110 | module.exports = Perf; 111 | -------------------------------------------------------------------------------- /lib/utils/scenes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * ScenesUtil - A scene manager with some functions. 5 | * 6 | * ####Example: 7 | * 8 | * ScenesUtil.isSingle(creator) 9 | * 10 | * 11 | * @class 12 | */ 13 | const forEach = require('lodash/forEach'); 14 | 15 | const ScenesUtil = { 16 | isSingle(creator) { 17 | const { scenes } = creator; 18 | const conf = creator.rootConf(); 19 | const speed = conf.getVal('speed'); 20 | return speed === 'high' && scenes.length === 1; 21 | }, 22 | 23 | hasTransition(creator) { 24 | const scene0 = creator.scenes[0]; 25 | return scene0.transition; 26 | }, 27 | 28 | fillTransition(creator) { 29 | const { scenes } = creator; 30 | forEach(scenes, scene => scene.fillTransition()); 31 | }, 32 | 33 | getLength(creator) { 34 | const { scenes } = creator; 35 | return scenes.length; 36 | }, 37 | }; 38 | 39 | module.exports = ScenesUtil; 40 | -------------------------------------------------------------------------------- /lib/utils/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Utils - Utility function collection 5 | * 6 | * ####Example: 7 | * 8 | * const effect = Utils.mergeExclude(effect, conf, ["type"]); 9 | * const id = Utils.genId(this.type); 10 | * 11 | * @object 12 | */ 13 | 14 | const forEach = require('lodash/forEach'); 15 | const cache = {}; 16 | 17 | const Utils = { 18 | /** 19 | * Generate auto-increment id based on type 20 | * @param {string} type - type 21 | * @return {string} id xxxx_10 22 | * @public 23 | */ 24 | genId(type) { 25 | if (!cache[type]) cache[type] = 1; 26 | return type + String(cache[type]++); 27 | }, 28 | 29 | /** 30 | * Generate 24-bit random number 31 | * @return {string} uid adsfUsdfn2 32 | * @public 33 | */ 34 | uid() { 35 | return ( 36 | Math.random() 37 | .toString(36) 38 | .substr(-8) + 39 | Math.random() 40 | .toString(36) 41 | .substr(-8) 42 | ); 43 | }, 44 | 45 | /** 46 | * Delete an element of the array 47 | * @param {array} arr - target array 48 | * @param {any} elem - an element 49 | * @return {array} Original array 50 | * @public 51 | */ 52 | deleteArrayElement(arr, elem) { 53 | const index = arr.indexOf(elem); 54 | if (index > -1) arr.splice(index, 1); 55 | return arr; 56 | }, 57 | 58 | /** 59 | * Swap two elements of an array 60 | * @param {array} arr - target array 61 | * @param {any} elem1 - an element 62 | * @param {any} elem1 - other element 63 | * @return {array} Original array 64 | * @public 65 | */ 66 | swapArrayElement(arr, elem1, elem2) { 67 | const index1 = typeof elem1 === 'number' ? elem1 : arr.indexOf(elem1); 68 | const index2 = typeof elem2 === 'number' ? elem2 : arr.indexOf(elem2); 69 | const temp = arr[index1]; 70 | 71 | arr[index1] = arr[index2]; 72 | arr[index2] = temp; 73 | return arr; 74 | }, 75 | 76 | /** 77 | * Sort the array according to a certain key 78 | * @public 79 | */ 80 | sortArrayByKey(arr, key, val) { 81 | const carr = []; 82 | forEach(arr, elem => { 83 | if (elem[key] === val) { 84 | carr.unshift(elem); 85 | } else { 86 | carr.push(elem); 87 | } 88 | }); 89 | return carr; 90 | }, 91 | 92 | /** 93 | * Remove undefined empty elements 94 | * @param {object} obj - target object 95 | * @return {object} target object 96 | * @public 97 | */ 98 | deleteUndefined(obj) { 99 | for (let key in obj) { 100 | if (obj[key] === undefined) { 101 | delete obj[key]; 102 | } 103 | } 104 | return obj; 105 | }, 106 | 107 | floor(n, s = 2) { 108 | const k = Math.pow(10, s); 109 | return Math.floor(n * k) / k; 110 | }, 111 | 112 | floorObject(obj, s = 2) { 113 | for (let key in obj) { 114 | Utils.floor(obj[key], s); 115 | } 116 | return obj; 117 | }, 118 | 119 | sleep(ms) { 120 | return new Promise(resolve => setTimeout(resolve, ms)); 121 | }, 122 | 123 | /** 124 | * Destroy the elements in object 125 | * @param {object} obj - target object 126 | * @public 127 | */ 128 | destroyObj(obj) { 129 | if (typeof obj !== 'object') return; 130 | for (let key in obj) { 131 | delete obj[key]; 132 | } 133 | }, 134 | 135 | angleToRadian(angle, s = 4) { 136 | return this.floor((angle / 180) * Math.PI, s); 137 | }, 138 | 139 | angleToPI(angle, s = 4) { 140 | const pi = this.angleToRadian(angle, s); 141 | return `${pi}*PI`; 142 | }, 143 | 144 | replacePlusMinus(str) { 145 | return str 146 | .replace(/\+\-/gi, '-') 147 | .replace(/\-\+/gi, '-') 148 | .replace(/\-\-/gi, '+') 149 | .replace(/\+\+/gi, '+') 150 | .replace(/\(t\-0\)/gi, 't') 151 | .replace(/\(on\-0\)/gi, 'on'); 152 | }, 153 | 154 | /** 155 | * Fix wrong file directory path // -> / 156 | * @param {string} path - file directory path 157 | * @public 158 | */ 159 | fixFolderPath(path) { 160 | return path.replace(/\/\//gi, '/'); 161 | }, 162 | 163 | explan(json) { 164 | try { 165 | return JSON.stringify(json); 166 | } catch (e) { 167 | return json.error; 168 | } 169 | }, 170 | 171 | excludeBind(obj1, obj2 = {}, exclude = []) { 172 | for (let key in obj2) { 173 | if (exclude.indexOf(key) < 0 && obj2[key] !== undefined) obj1[key] = obj2[key]; 174 | } 175 | }, 176 | 177 | clone(obj) { 178 | return Object.assign({}, obj); 179 | }, 180 | }; 181 | 182 | module.exports = Utils; 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ffcreatorlite", 3 | "version": "2.5.1", 4 | "description": "FFCreatorLite is a lightweight and flexible short video production library", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "lint": "eslint ./lib --ext .js", 9 | "travis": "npm run lint", 10 | "examples": "node ./examples/", 11 | "doc": "docsify serve ./docs" 12 | }, 13 | "repository": "https://github.com/tnfe/FFCreatorLite", 14 | "homepage": "https://tnfe.github.io/FFCreator", 15 | "keywords": [ 16 | "video", 17 | "nodejs", 18 | "video_production" 19 | ], 20 | "license": "MIT", 21 | "dependencies": { 22 | "eventemitter3": "^4.0.7", 23 | "ffmpeg-probe": "^1.0.6", 24 | "fluent-ffmpeg": "^2.1.2", 25 | "fs-extra": "^9.0.1", 26 | "image-size": "^0.9.1", 27 | "lodash": "^4.17.20", 28 | "rmfr": "^2.0.0", 29 | "tempy": "^0.7.1" 30 | }, 31 | "devDependencies": { 32 | "colors": "^1.4.0", 33 | "babel-eslint": "^10.1.0", 34 | "eslint": "^6.8.0", 35 | "eslint-config-standard": "^14.1.0", 36 | "eslint-plugin-import": "^2.18.2", 37 | "eslint-plugin-node": "^10.0.0", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-standard": "^4.0.1", 40 | "inquirer": "^7.3.3", 41 | "jest": "^27.2.0", 42 | "keypress": "^0.2.1" 43 | }, 44 | "files": [ 45 | "dist", 46 | "lib" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /scripts/crop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # fade depends on loop otherwise invalid -loop 1 -i img 3 | 4 | ffmpeg -loop 1 -i imgs/003.jpeg -loop 1 -i imgs/logo.png \ 5 | -filter_complex \ 6 | "color=c=black:r=60:size=800*450:d=20.0[black];\ 7 | [0:v]format=yuva420p,scale=8000x4500,zoompan=z='zoom+0.002':d=25*14:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=800*450[bg0]; \ 8 | [1:v]format=yuva420p,crop=w=200:h=200:x=50*t:y=34[logo];\ 9 | [black][bg0]overlay[bg1];\ 10 | [bg1][logo]overlay=x=400:y=(H-h)/2" \ 11 | -ss 1 -t 20 -c:v libx264 -c:a aac crop.mp4 -------------------------------------------------------------------------------- /scripts/fade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # fade depends on loop otherwise invalid -loop 1 -i img 3 | 4 | ffmpeg -loop 1 -i imgs/003.jpeg -loop 1 -i imgs/logo.png \ 5 | -filter_complex \ 6 | "color=c=black:r=60:size=800*450:d=20.0[black];\ 7 | [0:v]format=yuva420p,scale=8000x4500,zoompan=z='zoom+0.002':d=25*14:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=800*450[bg0]; \ 8 | [1:v]format=yuva420p[logo1]; \ 9 | [logo1]fade=t=in:st=1.0:d=2.0:alpha=1,fade=t=out:st=8.0:d=2.0:alpha=1[logo];\ 10 | [black][bg0]overlay[bg1];\ 11 | [bg1][logo]overlay=x=400:y=(H-h)/2" \ 12 | -ss 1 -t 20 -c:v libx264 -c:a aac fade.mp4 -------------------------------------------------------------------------------- /scripts/move-out.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # https://superuser.com/questions/713498/how-to-show-overlay-image-in-a-certain-time-span-with-ffmpeg 3 | # disappear enable=between(t\,0\,11) 4 | 5 | ffmpeg -threads 2 -loop 1 -i imgs/003.jpeg -t 12 -loop 1 -i imgs/logo.png -t 11 \ 6 | -filter_complex \ 7 | "color=c=black:r=60:size=800*450:d=20.0[black];\ 8 | [0:v]format=yuva420p,scale=8000x4500,zoompan=z='zoom+0.002':d=25*12:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=800*450[bg0]; \ 9 | [1:v]format=yuva420p,scale=4000:4000,setsar=1/1,zoompan=z='zoom+0.002':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=125,trim=duration=5[v1];[v1]scale=200:200[logo1]; \ 10 | [black][bg0]overlay=eof_action=pass[bg1];\ 11 | [bg1][logo1]overlay=x='if(between(t,0,5),-w+(W+w)/2/5*t,if(between(t,8,11),(W-w)/2 - 60*(t-8),(W-w)/2))':y=(H-h)/2:enable=between(t\,0\,11)" \ 12 | -ss 1 -t 20 -c:v libx264 -c:a aac move-out.mp4 -------------------------------------------------------------------------------- /scripts/move.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Does not rely on zoompan 3 | 4 | ffmpeg -threads 2 -i imgs/003.jpeg -i imgs/logo.png \ 5 | -filter_complex \ 6 | "[0:v]format=yuva420p,scale=8000x4500,zoompan=z='zoom+0.002':d=25*14:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=800*450[bg0]; \ 7 | [1:v]format=yuva420p[logo1]; \ 8 | [bg0][logo1]overlay=x='if(between(t,0,4),-300+700*(2*t/4-pow(t/4,2)),400)':y=(H-h)/2:enable='between(t,2,10)'" \ 9 | -ss 1 -t 20 -c:v libx264 -c:a aac move.mp4 -------------------------------------------------------------------------------- /scripts/rotate-if.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Conditional rotation animation 3 | 4 | ffmpeg -i imgs/001.jpeg -loop 1 -t 20 -i imgs/logo.png \ 5 | -filter_complex \ 6 | "[0:v]format=yuva420p,scale=8000x4500,zoompan=z='zoom+0.002':d=25*14:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=800*450[bg0]; \ 7 | [1:v]format=yuva420p,rotate=a='if(lte(t,5),PI*2/10*t)':ow=hypot(iw\,ih):oh=ow:c=black@0[logo1]; \ 8 | [logo1]fade=t=in:st=1.0:d=2.0:alpha=1[logo];\ 9 | [bg0][logo]overlay=x='400-t*60':y=(H-h)/2" \ 10 | -ss 1 -t 20 -c:v libx264 -c:a aac rotateif.mp4 -------------------------------------------------------------------------------- /scripts/rotate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # https://blog.csdn.net/yu540135101/article/details/93386083 3 | 4 | ffmpeg -i imgs/003.jpeg -loop 1 -t 20 -i imgs/logo.png \ 5 | -filter_complex \ 6 | "[0:v]format=yuva420p,scale=8000x4500,zoompan=z='zoom+0.002':d=25*14:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=800*450[bg0]; \ 7 | [1:v]format=yuva420p,rotate=PI*2/10*t:ow=hypot(iw\,ih):oh=ow:c=0x00000000[logo1]; \ 8 | [logo1]fade=t=in:st=1.0:d=2.0:alpha=1[logo];\ 9 | [bg0][logo]overlay=x=400:y=(H-h)/2" \ 10 | -ss 1 -t 20 -c:v libx264 -c:a aac rotate.mp4 -------------------------------------------------------------------------------- /scripts/s+r+m.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Solve rotate jitter 3 | 4 | ffmpeg -threads 2 -loop 1 -i imgs/003.jpeg -loop 1 -i imgs/logo.png \ 5 | -filter_complex \ 6 | "color=c=black:r=60:size=800*450:d=20.0[black];\ 7 | [0:v]format=yuva420p,pad=2*iw:2*ih:(ow-iw)/2:(oh-ih)/2:color=black@0,scale=8000x4500,zoompan=z='if(lte(zoom,1.5),zoom+0.005,1.51)':d=25*10:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1600*900,setpts=PTS-STARTPTS+0/TB,setsar=1/1[bg0]; \ 8 | [1:v]scale=4000x4000,pad=1.5*iw:1.5*ih:(ow-iw)/2:(oh-ih)/2:color=black@0,zoompan=z='zoom+0.002':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':fps=25:d=125:s=200x200,setpts=PTS-STARTPTS+0/TB[logo1]; \ 9 | [logo1]fade=t=in:st=1.0:d=2.0:alpha=1[logo2];\ 10 | [logo2]format=yuva420p,rotate=PI*2/10*t:ow=hypot(iw\,ih):oh=ow:c=0x00000000[logo];\ 11 | [black][bg0]overlay=x=-overlay_w/4:y=-overlay_h/4[bg1];\ 12 | [bg1][logo]overlay=x=100+t*20:y=100" \ 13 | -ss 1 -t 20 -c:v libx264 -c:a aac srm.mp4 -------------------------------------------------------------------------------- /scripts/zoom.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # zoompan抖动 - 前加scale=4000x4000, 4 | # zoompan条件 - zoompan=z='if(lte(zoom,1.5),zoom+0.005,1.51)' 5 | # loop 1和zoompan冲突会循环缩放 - 设置setpts=PTS-STARTPTS+12/TB 6 | # zoompan 被裁切解决办法 - pad=scale*iw:scale*ih:(ow-iw)/2:(oh-ih)/2:color=black@0, 在overlay再左移归位 7 | 8 | ffmpeg -threads 2 -loop 1 -i imgs/003.jpeg -loop 1 -i imgs/logo.png \ 9 | -filter_complex \ 10 | "color=c=black:r=60:size=800*450:d=20.0[black];\ 11 | [0:v]format=yuva420p,pad=2*iw:2*ih:(ow-iw)/2:(oh-ih)/2:color=black@0,scale=8000x4500,zoompan=z='if(lte(zoom,1.5),zoom+0.005,1.51)':d=25*10:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1600*900,setpts=PTS-STARTPTS+0/TB,setsar=1/1[bg0]; \ 12 | [1:v]scale=4000x4000,pad=1.5*iw:1.5*ih:(ow-iw)/2:(oh-ih)/2:color=black@0,zoompan=z='zoom+0.002':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':fps=25:d=125:s=200x200,setpts=PTS-STARTPTS+0/TB[logo1]; \ 13 | [logo1]fade=t=in:st=1.0:d=2.0:alpha=1[logo];\ 14 | [black][bg0]overlay=x=-overlay_w/4:y=-overlay_h/4[bg1];\ 15 | [bg1][logo]overlay=x=100:y=100" \ 16 | -ss 1 -t 20 -c:v libx264 -c:a aac zoom.mp4 -------------------------------------------------------------------------------- /scripts/zoomloop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ffmpeg -threads 2 -loop 1 -i imgs/001.jpeg -loop 1 -i imgs/logo.png \ 4 | -filter_complex \ 5 | "color=c=black:r=60:size=800*450:d=20.0[black];\ 6 | [0:v]format=yuva420p,pad=2*iw:2*ih:(ow-iw)/2:(oh-ih)/2:color=black@0,scale=8000x4500,\ 7 | zoompan=z='if(between(on,0,25*5),zoom+0.01,2.25)':d=25*10:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1600*900,setpts=PTS-STARTPTS+0/TB,setsar=1/1[bg0]; \ 8 | [1:v]scale=4000x4000,pad=1.5*iw:1.5*ih:(ow-iw)/2:(oh-ih)/2:color=black@0,\ 9 | zoompan=z='zoom+0.002':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':fps=25:d=125:s=200x200,setpts=PTS-STARTPTS+0/TB[logo1]; \ 10 | [logo1]fade=t=in:st=1.0:d=2.0:alpha=1[logo];\ 11 | [black][bg0]overlay=x=-overlay_w/4:y=-overlay_h/4[bg1];\ 12 | [bg1][logo]overlay=x=100:y=100" \ 13 | -ss 1 -t 20 -c:v libx264 -c:a aac zoomloop.mp4 14 | -------------------------------------------------------------------------------- /test/unit/conf/conf.test.js: -------------------------------------------------------------------------------- 1 | const Conf = require('@/conf/conf.js') 2 | 3 | describe('conf/conf', ()=> { 4 | let conf = null 5 | test('initialize should be success', ()=> { 6 | conf = new Conf({pathId: 1}) 7 | expect(conf).toBeInstanceOf(Conf) 8 | }) 9 | 10 | test('getVal: should return correct value', ()=> { 11 | expect(conf.getVal('debug')).toBeFalsy() 12 | expect(conf.getVal('defaultOutputOptions')).toBeTruthy() 13 | expect(conf.getVal('width')).toBe(800) 14 | }) 15 | 16 | test('setVal: should set value success', ()=> { 17 | conf.setVal('debug', true) 18 | expect(conf.getVal('debug')).toBeTruthy() 19 | }) 20 | 21 | test('getWH: should return width.height', ()=> { 22 | const wh = conf.getWH('.') 23 | expect(wh).toBe('800.450') 24 | }) 25 | 26 | test('getCacheDir: should return dir path', ()=> { 27 | expect(conf.getCacheDir()).toMatch('/') 28 | }) 29 | 30 | test('copyByDefaultVal: should copy value success', ()=> { 31 | conf.copyByDefaultVal({}, 'test', 'test', 'test') 32 | const test = conf.getVal('test') 33 | expect(test).toBe('test') 34 | }) 35 | 36 | test('getFakeConf: should reutn fakeConf', ()=> { 37 | const fakeConf = Conf.getFakeConf() 38 | expect(fakeConf.getVal).toBeInstanceOf(Function) 39 | expect(fakeConf.setVal).toBeInstanceOf(Function) 40 | }) 41 | }) -------------------------------------------------------------------------------- /test/unit/core/base.test.js: -------------------------------------------------------------------------------- 1 | const FFBase = require('@/core/base'); 2 | 3 | jest.mock('events'); 4 | jest.mock('@/conf/conf', () => ({ 5 | getFakeConf: jest.fn(() => ({})), 6 | })); 7 | jest.mock('@/utils/utils', () => ({ 8 | generateID: jest.fn(() => 1), 9 | genId: jest.fn(() => 1), 10 | })); 11 | 12 | describe('core/base', () => { 13 | let base = null; 14 | base = new FFBase(); 15 | 16 | test('instantiation component needs to succeed', () => { 17 | expect(base).toBeInstanceOf(FFBase); 18 | }); 19 | 20 | test('generateID: set id success', () => { 21 | base.genId(); 22 | expect(base.id).toBe(1); 23 | }); 24 | 25 | test('root: should return self', () => { 26 | expect(base.root()).toBe(base); 27 | }); 28 | 29 | test('rootConf: should return conf', () => { 30 | const conf = base.rootConf(); 31 | expect(conf).toMatchObject({}); 32 | }); 33 | 34 | test('destroy: destroy function invoke success', () => { 35 | base.destroy(); 36 | expect(base.parent).toBeFalsy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/unit/node/node.test.js: -------------------------------------------------------------------------------- 1 | const FFNode = require('@/node/node'); 2 | 3 | jest.mock('events'); 4 | jest.mock('@/conf/conf', () => ({ 5 | getFakeConf: jest.fn(() => ({})), 6 | })); 7 | jest.mock('@/utils/utils', () => ({ 8 | generateID: jest.fn(() => 1), 9 | genId: jest.fn(() => 1), 10 | })); 11 | 12 | describe('node/node', () => { 13 | let node = null; 14 | node = new FFNode(); 15 | 16 | test('instantiation component needs to succeed', () => { 17 | expect(node).toBeInstanceOf(FFNode); 18 | }); 19 | 20 | test('generateID: set id success', () => { 21 | node.genId(); 22 | expect(node.id).toBe(1); 23 | }); 24 | 25 | test('root: should return self', () => { 26 | expect(node.root()).toBe(node); 27 | }); 28 | 29 | test('rootConf: should return conf', () => { 30 | const conf = node.rootConf(); 31 | expect(conf).toMatchObject({}); 32 | }); 33 | 34 | test('destroy: destroy function invoke success', () => { 35 | node.destroy(); 36 | expect(node.parent).toBeFalsy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/unit/utils/ffmpeg.test.js: -------------------------------------------------------------------------------- 1 | const fFmpegUtil = require('@/utils/ffmpeg'); 2 | const ffmpeg = require('fluent-ffmpeg'); 3 | 4 | jest.mock('fluent-ffmpeg', () => { 5 | const ffmpegMock = jest.fn(() => ({})); 6 | ffmpegMock.setFfmpegPath = jest.fn(() => ({})); 7 | ffmpegMock.setFfprobePath = jest.fn(() => ({})); 8 | 9 | return ffmpegMock; 10 | }); 11 | 12 | describe('utils/ffmpeg', () => { 13 | test('getFFmpeg: should return ffmpeg', () => { 14 | const instance = fFmpegUtil.getFFmpeg(); 15 | expect(instance).toBe(ffmpeg); 16 | }); 17 | 18 | test('setFFmpegPath: invoking ffmpeg.setFFmpegPath once', () => { 19 | fFmpegUtil.setFFmpegPath('/'); 20 | expect(ffmpeg.setFfmpegPath).toBeCalledTimes(1); 21 | }); 22 | 23 | test('setFFprobePath: invoking ffmpeg.setFFprobePath once', () => { 24 | fFmpegUtil.setFFprobePath('/'); 25 | expect(ffmpeg.setFfprobePath).toBeCalledTimes(1); 26 | }); 27 | 28 | describe('concatOpts: extra config should insert opts', () => { 29 | test('extra config is Array', () => { 30 | const opts = [{name: 'test'}]; 31 | const arr = [{name: 'test1'}, {name: 'test2'}]; 32 | fFmpegUtil.concatOpts(opts, arr); 33 | expect(opts.length).toBe(3); 34 | }); 35 | 36 | test('extra config is Object', () => { 37 | const opts = [{name: 'test'}]; 38 | const obj = {name: 'test1'}; 39 | fFmpegUtil.concatOpts(opts, obj); 40 | expect(opts.length).toBe(2); 41 | }); 42 | }); 43 | 44 | test('createCommand: should return command', () => { 45 | fFmpegUtil.createCommand(); 46 | expect(ffmpeg).toBeCalledTimes(1); 47 | }); 48 | }); 49 | --------------------------------------------------------------------------------