├── .gitignore ├── .project ├── Gruntfile.js ├── LICENSE ├── README.md ├── demos ├── audio_extract.js ├── ffmpeg_test.js ├── image_extract.js ├── temp.js ├── video_faststart.js ├── video_info.js ├── video_info_raw.js ├── video_playlist.js ├── video_to_gif.js ├── video_transcode.js ├── video_transcode_to_avi.js ├── video_transcode_to_mp4_av1.js └── video_watermark.js ├── index.js ├── jsonize.js ├── package.json ├── src ├── ffmpeg-faststart.js ├── ffmpeg-graceful.js ├── ffmpeg-helpers.js ├── ffmpeg-multi-pass.js ├── ffmpeg-playlist.js ├── ffmpeg-simple.js ├── ffmpeg-test.js ├── ffmpeg-volume-detect.js ├── ffmpeg.js ├── ffprobe-simple.js └── ffprobe.js ├── tests ├── assets │ ├── audio.mp3 │ ├── etc_passwd_xbin.avi │ ├── iphone_rotated.mov │ ├── logo.png │ ├── novideo.mp4 │ └── video-640-360.mp4 └── tests │ ├── ffmpeg-simple-gif.js │ ├── ffmpeg-simple-image.js │ ├── ffmpeg-simple-rotation.js │ ├── ffmpeg-simple-sizing.js │ ├── ffmpeg-simple-watermarks.js │ ├── ffmpeg-volume-detect.js │ ├── ffmpeg.js │ ├── ffprobe-simple.js │ ├── ffprobe.js │ └── settings.js └── types ├── betajs.d.ts ├── ffmpeg-faststart.d.ts ├── ffmpeg-graceful.d.ts ├── ffmpeg-multi-pass.d.ts ├── ffmpeg-playlist.d.ts ├── ffmpeg-simple.d.ts ├── ffmpeg-test.d.ts ├── ffmpeg-volume-detect.d.ts ├── ffmpeg.d.ts ├── ffprobe-simple.d.ts ├── ffprobe.d.ts ├── index.d.ts └── opts.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | temp 3 | npm-debug.log 4 | .idea 5 | tmp 6 | package-lock.json 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | JS FFMpeg 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg : grunt.file.readJSON('package.json'), 5 | shell : { 6 | qunit : { 7 | command: ["node_modules/qunitjs/bin/qunit"].concat(["index.js", "tests/tests/*.js"]).join(" "), 8 | options: { 9 | stdout: true, 10 | stderr: true 11 | }, 12 | src: [ 13 | "index.js", "tests/tests/*.js" 14 | ] 15 | } 16 | }, 17 | jshint : { 18 | options : { 19 | esversion: 6 20 | }, 21 | source : [ "./Gruntfile.js", "./index.js", "./tests/tests/*.js", 22 | "./src/*.js" ] 23 | } 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-shell'); 27 | grunt.loadNpmTasks('grunt-contrib-jshint'); 28 | 29 | grunt.registerTask('default', [ 'jshint', 'shell:qunit' ]); 30 | 31 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Jsonize. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-ffmpeg 2 | 3 | This is a simple wrapper for FFMPEG and FFPROBE. 4 | 5 | 6 | ## Getting Started 7 | 8 | 9 | ```javascript 10 | git clone https://github.com/jsonize/js-ffmpeg.git 11 | npm install 12 | grunt 13 | ``` 14 | 15 | 16 | 17 | ## Basic Usage 18 | 19 | 20 | ```javascript 21 | ffmpeg = require('js-ffmpeg'); 22 | 23 | // raw call of ffprobe 24 | ffmpeg.ffprobe('video.mp4').success(function (json) { 25 | console.log(json); 26 | }).error(function (error) { 27 | console.log(error); 28 | }); 29 | 30 | // improved and simplified values and errors 31 | ffmpeg.ffprobe_simple('video.mp4').success(function (json) { 32 | console.log(json); 33 | }).error(function (error) { 34 | console.log(error); 35 | }); 36 | 37 | // raw call of ffmpeg (source(s), arguments, target, progress callback) 38 | ffmpeg.ffmpeg('video.mp4', [...], 'output.mp4', function (progress) { 39 | console.log(progress); 40 | }).success(function (json) { 41 | console.log(json); 42 | }).error(function (error) { 43 | console.log(error); 44 | }); 45 | 46 | // improved and simplified call of ffmpeg (source(s), arguments, target, progress callback) 47 | ffmpeg.ffmpeg_simple('video.mp4', { 48 | width: 640, 49 | height: 360, 50 | auto_rotate: true, 51 | ratio_strategy: "fixed", 52 | shrink_strategy: "crop", 53 | mixed_strategy: "crop-pad", 54 | stretch_strategy: "pad" 55 | }, 'output.mp4', function (progress) { 56 | console.log(progress); 57 | }).success(function (json) { 58 | console.log(json); 59 | }).error(function (error) { 60 | console.log(error); 61 | }); 62 | ``` 63 | 64 | 65 | ## Contributors 66 | 67 | - Ziggeo 68 | - Oliver Friedmann 69 | 70 | 71 | ## License 72 | 73 | Apache-2.0 74 | 75 | -------------------------------------------------------------------------------- /demos/audio_extract.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FILE", "target audio"], 6 | ["", "docker=CONTAINER", "docker"] 7 | ]).bindHelp().parseSystem().options; 8 | 9 | jsffmpeg.ffmpeg_simple(args.source, { 10 | output_type: "audio" 11 | }, args.target, null, null, { 12 | docker: args.docker, 13 | test_ffmpeg: true 14 | }).success(function (data) { 15 | console.log(data); 16 | }).error(function (error) { 17 | console.log(error); 18 | }); -------------------------------------------------------------------------------- /demos/ffmpeg_test.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "docker=CONTAINER", "docker"] 5 | ]).bindHelp().parseSystem().options; 6 | 7 | jsffmpeg.ffmpeg_test({ 8 | docker: args.docker 9 | }).success(function (data) { 10 | console.log(data); 11 | }).error(function (error) { 12 | console.log(error); 13 | }); -------------------------------------------------------------------------------- /demos/image_extract.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FILE", "target image"], 6 | ["", "docker=CONTAINER", "docker"] 7 | ]).bindHelp().parseSystem().options; 8 | 9 | jsffmpeg.ffmpeg_simple(args.source, { 10 | output_type: "image", 11 | image_percentage: 0.5 12 | }, args.target, null, null, { 13 | docker: args.docker, 14 | test_ffmpeg: true 15 | }).success(function (data) { 16 | console.log(data); 17 | }).error(function (error) { 18 | console.log(error); 19 | }); -------------------------------------------------------------------------------- /demos/temp.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FILE", "target video"], 6 | ["", "docker=CONTAINER", "docker"] 7 | ]).bindHelp().parseSystem().options; 8 | 9 | jsffmpeg.ffmpeg_simple(args.source, { 10 | width: 1280, 11 | height: 720, 12 | ratio_strategy: "stretch", 13 | size_strategy: "keep", 14 | shrink_strategy: "shrink-pad", 15 | stretch_strategy: "stretch-pad", 16 | mixed_strategy: "shrink-pad" 17 | }, args.target, null, null, { 18 | docker: args.docker, 19 | test_ffmpeg: true 20 | }).success(function (data) { 21 | console.log(data); 22 | }).error(function (error) { 23 | console.log(error); 24 | }); -------------------------------------------------------------------------------- /demos/video_faststart.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FILE", "target video"] 6 | ]).bindHelp().parseSystem().options; 7 | 8 | jsffmpeg.ffmpeg_faststart(args.source, args.target, null, null, { 9 | /* 10 | docker: { 11 | "container" : "jrottenberg/ffmpeg", 12 | "proxy": "localhost:1234", 13 | "replaceArguments": { 14 | "libfaac": "libfdk_aac", 15 | "^/var": "/private/var" 16 | }, 17 | "preprocessFiles": { 18 | "chown": "USERNAME", 19 | "chmod": 666, 20 | "mkdirs": true 21 | }, 22 | "postprocessFiles": { 23 | "chown": "daemon", 24 | "chmod": 666, 25 | "recoverChown": true, 26 | "recoverChmod": true 27 | } 28 | }, 29 | "test_info" : { 30 | "capabilities" : { 31 | "auto_rotate" : true 32 | }, 33 | "encoders" : ["libdfk_aac", "aac"] 34 | }, 35 | */ 36 | test_ffmpeg: true, 37 | timeout: args.timeout 38 | }).success(function (data) { 39 | console.log(data); 40 | }).error(function (error) { 41 | console.log(error); 42 | }); -------------------------------------------------------------------------------- /demos/video_info.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "source=FILE", "source video"] , 5 | ["", "docker=CONTAINER", "docker"] 6 | ]).bindHelp().parseSystem().options; 7 | 8 | jsffmpeg.ffprobe_simple(args.source, { 9 | docker: args.docker 10 | }).success(function (data) { 11 | console.log(data); 12 | }).error(function (error) { 13 | console.log(error); 14 | }); -------------------------------------------------------------------------------- /demos/video_info_raw.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "source=FILE", "source video"] , 5 | ["", "docker=CONTAINER", "docker"] 6 | ]).bindHelp().parseSystem().options; 7 | 8 | jsffmpeg.ffprobe(args.source, { 9 | docker: args.docker 10 | }).success(function (data) { 11 | console.log(data); 12 | }).error(function (error) { 13 | console.log(error); 14 | }); -------------------------------------------------------------------------------- /demos/video_playlist.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require("node-getopt").create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FOLDER", "target folder"], 6 | ["", "renditions=OBJECT", "renditions object"], 7 | ["", "watermark=FILE", "watermark image"], 8 | ["", "docker=CONTAINER", "docker"], 9 | ["", "timeout=MS", "timeout"], 10 | ["", "ratiostrategy=STRATEGY", "ratio strategy", "fixed"], 11 | ["", "sizestrategy=STRATEGY", "size strategy", "keep"], 12 | ["", "shrinkstrategy=STRATEGY", "shrink strategy", "shrink-pad"], 13 | ["", "stretchstrategy=STRATEGY", "stretch strategy", "pad"], 14 | ["", "mixedstrategy=STRATEGY", "mixed strategy", "shrink-pad"] 15 | ]).bindHelp().parseSystem().options; 16 | const renditions = args.renditions || [ 17 | {resolution: "640x360", bitrate: 800, audio_rate: 96}, 18 | {resolution: "842x480", bitrate: 1400, audio_rate: 128}, 19 | {resolution: "1280x720", bitrate: 2800, audio_rate: 128} 20 | ]; 21 | jsffmpeg.ffmpeg_playlist(args.source, { 22 | watermark: args.watermark, 23 | renditions: renditions, 24 | //normalize_audio: true 25 | ratio_strategy: args.ratiostrategy, 26 | size_strategy: args.sizestrategy, 27 | shrink_strategy: args.shrinkstrategy, 28 | stretch_strategy: args.stretchstrategy, 29 | mixed_strategy: args.mixedstrategy 30 | }, args.target, null, null, { 31 | test_ffmpeg: true, 32 | timeout: args.timeout 33 | /*docker: { 34 | "container" : "jrottenberg/ffmpeg", 35 | "proxy": "localhost:1234", 36 | "replaceArguments": { 37 | "libfaac": "libfdk_aac", 38 | "^/var": "/private/var" 39 | }, 40 | "preprocessFiles": { 41 | "chown": "USERNAME", 42 | "chmod": 666, 43 | "mkdirs": true 44 | }, 45 | "postprocessFiles": { 46 | "chown": "daemon", 47 | "chmod": 666, 48 | "recoverChown": true, 49 | "recoverChmod": true 50 | } 51 | }, 52 | "test_info" : { 53 | "capabilities" : { 54 | "auto_rotate" : true 55 | }, 56 | "encoders" : ["libdfk_aac", "aac"] 57 | }*/ 58 | }).success(function(data) { 59 | console.log(data); 60 | }).error(function(error) { 61 | console.log(error); 62 | }); -------------------------------------------------------------------------------- /demos/video_to_gif.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FILE", "target image"], 6 | ["", "docker=CONTAINER", "docker"] 7 | ]).bindHelp().parseSystem().options; 8 | 9 | jsffmpeg.ffmpeg_simple(args.source, { 10 | output_type: "gif", 11 | framerate: 12, 12 | width: 480, 13 | time_start: 0, 14 | time_end: 2 15 | }, args.target, null, null, { 16 | docker: args.docker, 17 | test_ffmpeg: true 18 | }).success(function (data) { 19 | console.log(data); 20 | }).error(function (error) { 21 | console.log(error); 22 | }); 23 | -------------------------------------------------------------------------------- /demos/video_transcode.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FILE", "target video"], 6 | ["", "width=WIDTH", "target width"], 7 | ["", "height=HEIGHT", "target height"], 8 | ["", "watermark=FILE", "watermark image"], 9 | ["", "docker=CONTAINER", "docker"], 10 | ["", "timeout=MS", "timeout"], 11 | ["", "ratiostrategy=STRATEGY", "ratio strategy", "fixed"], 12 | ["", "sizestrategy=STRATEGY", "size strategy", "keep"], 13 | ["", "shrinkstrategy=STRATEGY", "shrink strategy", "shrink-pad"], 14 | ["", "stretchstrategy=STRATEGY", "stretch strategy", "pad"], 15 | ["", "mixedstrategy=STRATEGY", "mixed strategy", "shrink-pad"] 16 | ]).bindHelp().parseSystem().options; 17 | 18 | jsffmpeg.ffmpeg_graceful(args.source, { 19 | width: parseInt(args.width) || 640, 20 | height: parseInt(args.height) || 360, 21 | watermark: args.watermark, 22 | //normalize_audio: true 23 | ratio_strategy: args.ratiostrategy, 24 | size_strategy: args.sizestrategy, 25 | shrink_strategy: args.shrinkstrategy, 26 | stretch_strategy: args.stretchstrategy, 27 | mixed_strategy: args.mixedstrategy 28 | }, args.target, null, null, { 29 | /* 30 | docker: { 31 | "container" : "jrottenberg/ffmpeg", 32 | "proxy": "localhost:1234", 33 | "replaceArguments": { 34 | "libfaac": "libfdk_aac", 35 | "^/var": "/private/var" 36 | }, 37 | "preprocessFiles": { 38 | "chown": "USERNAME", 39 | "chmod": 666, 40 | "mkdirs": true 41 | }, 42 | "postprocessFiles": { 43 | "chown": "daemon", 44 | "chmod": 666, 45 | "recoverChown": true, 46 | "recoverChmod": true 47 | } 48 | }, 49 | "test_info" : { 50 | "capabilities" : { 51 | "auto_rotate" : true 52 | }, 53 | "encoders" : ["libdfk_aac", "aac"] 54 | }, 55 | */ 56 | test_ffmpeg: true, 57 | timeout: args.timeout 58 | }).success(function (data) { 59 | console.log(data); 60 | }).error(function (error) { 61 | console.log(error); 62 | }); -------------------------------------------------------------------------------- /demos/video_transcode_to_avi.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require("node-getopt").create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FILE", "target video"], 6 | ["", "width=WIDTH", "target width"], 7 | ["", "height=HEIGHT", "target height"], 8 | ["", "watermark=FILE", "watermark image"], 9 | ["", "docker=CONTAINER", "docker"], 10 | ["", "timeout=MS", "timeout"], 11 | ["", "ratiostrategy=STRATEGY", "ratio strategy", "fixed"], 12 | ["", "sizestrategy=STRATEGY", "size strategy", "keep"], 13 | ["", "shrinkstrategy=STRATEGY", "shrink strategy", "shrink-pad"], 14 | ["", "stretchstrategy=STRATEGY", "stretch strategy", "pad"], 15 | ["", "mixedstrategy=STRATEGY", "mixed strategy", "shrink-pad"] 16 | ]).bindHelp().parseSystem().options; 17 | 18 | jsffmpeg.ffmpeg_graceful(args.source, { 19 | width: parseInt(args.width) || 640, 20 | height: parseInt(args.height) || 360, 21 | video_format: "avi" 22 | }, args.target, null, null, { 23 | test_ffmpeg: true, 24 | timeout: args.timeout 25 | }).success(function(data) { 26 | console.log(data); 27 | }).error(function(error) { 28 | console.log(error); 29 | }); -------------------------------------------------------------------------------- /demos/video_transcode_to_mp4_av1.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require("node-getopt").create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FILE", "target video"], 6 | ["", "width=WIDTH", "target width"], 7 | ["", "height=HEIGHT", "target height"], 8 | ["", "watermark=FILE", "watermark image"], 9 | ["", "docker=CONTAINER", "docker"], 10 | ["", "timeout=MS", "timeout"], 11 | ["", "ratiostrategy=STRATEGY", "ratio strategy", "fixed"], 12 | ["", "sizestrategy=STRATEGY", "size strategy", "keep"], 13 | ["", "shrinkstrategy=STRATEGY", "shrink strategy", "shrink-pad"], 14 | ["", "stretchstrategy=STRATEGY", "stretch strategy", "pad"], 15 | ["", "mixedstrategy=STRATEGY", "mixed strategy", "shrink-pad"] 16 | ]).bindHelp().parseSystem().options; 17 | 18 | jsffmpeg.ffmpeg_graceful(args.source, { 19 | width: parseInt(args.width) || 640, 20 | height: parseInt(args.height) || 360, 21 | video_format: "mp4-av1" 22 | }, args.target, null, null, { 23 | test_ffmpeg: true, 24 | timeout: args.timeout 25 | }).success(function(data) { 26 | console.log(data); 27 | }).error(function(error) { 28 | console.log(error); 29 | }); -------------------------------------------------------------------------------- /demos/video_watermark.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/../index.js"); 2 | 3 | var args = require('node-getopt').create([ 4 | ["", "source=FILE", "source video"], 5 | ["", "target=FILE", "target video"], 6 | ["", "watermark=FILE", "watermark image"], 7 | ["", "timeout=MS", "timeout"], 8 | ["", "watermarksize=SIZE", "watermarksize"], 9 | ["", "watermarkx=WATERMARKX", "watermarkx"], 10 | ["", "watermarky=WATERMARKY", "watermarky"] 11 | ]).bindHelp().parseSystem().options; 12 | 13 | jsffmpeg.ffmpeg_graceful(args.source, { 14 | watermark: args.watermark, 15 | watermark_size: parseFloat(args.watermarksize), 16 | watermark_x: parseFloat(args.watermarkx), 17 | watermark_y: parseFloat(args.watermarky) 18 | }, args.target, null, null, { 19 | test_ffmpeg: true, 20 | timeout: args.timeout 21 | }).success(function (data) { 22 | console.log(data); 23 | }).error(function (error) { 24 | console.log(error); 25 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | Scoped = global.Scoped || require("betajs-scoped"); 2 | BetaJS = global.BetaJS || require("betajs"); 3 | Scoped.binding("betajs", "global:BetaJS"); 4 | 5 | module.exports = { 6 | 7 | ffprobe: require(__dirname + "/src/ffprobe.js").ffprobe, 8 | 9 | ffmpeg: require(__dirname + "/src/ffmpeg.js").ffmpeg, 10 | 11 | ffprobe_simple: require(__dirname + "/src/ffprobe-simple.js").ffprobe_simple, 12 | 13 | ffmpeg_simple_raw: require(__dirname + "/src/ffmpeg-simple.js").ffmpeg_simple_raw, 14 | 15 | ffmpeg_simple: require(__dirname + "/src/ffmpeg-simple.js").ffmpeg_simple, 16 | 17 | ffmpeg_faststart: require(__dirname + "/src/ffmpeg-faststart.js").ffmpeg_faststart, 18 | 19 | ffmpeg_graceful: require(__dirname + "/src/ffmpeg-graceful.js").ffmpeg_graceful, 20 | 21 | ffmpeg_volume_detect: require(__dirname + "/src/ffmpeg-volume-detect.js").ffmpeg_volume_detect, 22 | 23 | ffmpeg_test: require(__dirname + "/src/ffmpeg-test.js").ffmpeg_test, 24 | 25 | ffmpeg_multi_pass: require(__dirname + "/src/ffmpeg-multi-pass.js").ffmpeg_multi_pass, 26 | 27 | ffmpeg_playlist: require(__dirname + "/src/ffmpeg-playlist.js").ffmpeg_playlist, 28 | 29 | ffmpeg_playlist_raw: require(__dirname + "/src/ffmpeg-playlist.js").ffmpeg_playlist_raw 30 | 31 | }; -------------------------------------------------------------------------------- /jsonize.js: -------------------------------------------------------------------------------- 1 | var jsffmpeg = require(__dirname + "/index.js"); 2 | 3 | 4 | Scoped.define("jsonize:JsonizeFfprobeTask", [ 5 | "jsonize:AbstractJsonizeTask", 6 | "jsonize:JsonizeTaskRegistry", 7 | "betajs:Promise" 8 | ], function (Class, TaskRegistry, Promise, scoped) { 9 | var Cls = Class.extend({scoped: scoped}, { 10 | 11 | _run: function (payload) { 12 | return jsffmpeg.ffprobe(payload.source, { 13 | docker: payload.docker, 14 | timeout: payload.timeout, 15 | test_ffmpeg: payload.test_ffmpeg, 16 | test_info: payload.test_info 17 | }); 18 | } 19 | 20 | }); 21 | 22 | TaskRegistry.register("ffprobe", Cls); 23 | 24 | return Cls; 25 | }); 26 | 27 | 28 | Scoped.define("jsonize:JsonizeFfprobeSimpleTask", [ 29 | "jsonize:AbstractJsonizeTask", 30 | "jsonize:JsonizeTaskRegistry", 31 | "betajs:Promise" 32 | ], function (Class, TaskRegistry, Promise, scoped) { 33 | var Cls = Class.extend({scoped: scoped}, { 34 | 35 | _run: function (payload) { 36 | return jsffmpeg.ffprobe_simple(payload.source, { 37 | docker: payload.docker, 38 | timeout: payload.timeout, 39 | test_ffmpeg: payload.test_ffmpeg, 40 | test_info: payload.test_info 41 | }); 42 | } 43 | 44 | }); 45 | 46 | TaskRegistry.register("ffprobe-simple", Cls); 47 | 48 | return Cls; 49 | }); 50 | 51 | 52 | Scoped.define("jsonize:JsonizeFfmpegTask", [ 53 | "jsonize:AbstractJsonizeTask", 54 | "jsonize:JsonizeTaskRegistry", 55 | "betajs:Promise" 56 | ], function (Class, TaskRegistry, Promise, scoped) { 57 | var Cls = Class.extend({scoped: scoped}, { 58 | 59 | _run: function (payload) { 60 | return jsffmpeg.ffmpeg( 61 | payload.source || payload.sources, 62 | payload.options || [], 63 | payload.output, 64 | this._event, 65 | this, 66 | { 67 | docker: payload.docker, 68 | timeout: payload.timeout, 69 | test_ffmpeg: payload.test_ffmpeg, 70 | test_info: payload.test_info 71 | } 72 | ); 73 | } 74 | 75 | }); 76 | 77 | TaskRegistry.register("ffmpeg", Cls); 78 | 79 | return Cls; 80 | }); 81 | 82 | 83 | Scoped.define("jsonize:JsonizeFfmpegMultiPassTask", [ 84 | "jsonize:AbstractJsonizeTask", 85 | "jsonize:JsonizeTaskRegistry", 86 | "betajs:Promise" 87 | ], function (Class, TaskRegistry, Promise, scoped) { 88 | var Cls = Class.extend({scoped: scoped}, { 89 | 90 | _run: function (payload) { 91 | return jsffmpeg.ffmpeg_multi_pass( 92 | payload.source || payload.sources, 93 | payload.options || [], 94 | 2, 95 | payload.output, 96 | this._event, 97 | this, 98 | { 99 | docker: payload.docker, 100 | timeout: payload.timeout, 101 | test_ffmpeg: payload.test_ffmpeg, 102 | test_info: payload.test_info 103 | } 104 | ); 105 | } 106 | 107 | }); 108 | 109 | TaskRegistry.register("ffmpeg-multi-pass", Cls); 110 | 111 | return Cls; 112 | }); 113 | 114 | 115 | Scoped.define("jsonize:JsonizeFfmpegSimpleTask", [ 116 | "jsonize:AbstractJsonizeTask", 117 | "jsonize:JsonizeTaskRegistry", 118 | "betajs:Promise" 119 | ], function (Class, TaskRegistry, Promise, scoped) { 120 | var Cls = Class.extend({scoped: scoped}, { 121 | 122 | _run: function (payload) { 123 | return jsffmpeg.ffmpeg_simple( 124 | payload.source || payload.sources, 125 | payload.options || {}, 126 | payload.output, 127 | this._event, 128 | this, 129 | { 130 | docker: payload.docker, 131 | timeout: payload.timeout, 132 | test_ffmpeg: payload.test_ffmpeg, 133 | test_info: payload.test_info 134 | } 135 | ); 136 | } 137 | 138 | }); 139 | 140 | TaskRegistry.register("ffmpeg-simple", Cls); 141 | 142 | return Cls; 143 | }); 144 | 145 | Scoped.define("jsonize:JsonizeFfmpegFaststartTask", [ 146 | "jsonize:AbstractJsonizeTask", 147 | "jsonize:JsonizeTaskRegistry", 148 | "betajs:Promise" 149 | ], function (Class, TaskRegistry, Promise, scoped) { 150 | var Cls = Class.extend({scoped: scoped}, { 151 | 152 | _run: function (payload) { 153 | return jsffmpeg.ffmpeg_faststart( 154 | payload.source || payload.sources, 155 | payload.output, 156 | this._event, 157 | this, 158 | { 159 | docker: payload.docker, 160 | timeout: payload.timeout, 161 | test_ffmpeg: payload.test_ffmpeg, 162 | test_info: payload.test_info 163 | } 164 | ); 165 | } 166 | 167 | }); 168 | 169 | TaskRegistry.register("ffmpeg-faststart", Cls); 170 | 171 | return Cls; 172 | }); 173 | 174 | 175 | Scoped.define("jsonize:JsonizeFfmpegGracefulTask", [ 176 | "jsonize:AbstractJsonizeTask", 177 | "jsonize:JsonizeTaskRegistry", 178 | "betajs:Promise" 179 | ], function (Class, TaskRegistry, Promise, scoped) { 180 | var Cls = Class.extend({scoped: scoped}, { 181 | 182 | _run: function (payload) { 183 | return jsffmpeg.ffmpeg_graceful( 184 | payload.source || payload.sources, 185 | payload.options || {}, 186 | payload.output, 187 | this._event, 188 | this, 189 | { 190 | docker: payload.docker, 191 | timeout: payload.timeout, 192 | test_ffmpeg: payload.test_ffmpeg, 193 | test_info: payload.test_info 194 | } 195 | ); 196 | } 197 | 198 | }); 199 | 200 | TaskRegistry.register("ffmpeg-graceful", Cls); 201 | 202 | return Cls; 203 | }); 204 | 205 | 206 | Scoped.define("jsonize:JsonizeFfmpegVolumeDetectTask", [ 207 | "jsonize:AbstractJsonizeTask", 208 | "jsonize:JsonizeTaskRegistry", 209 | "betajs:Promise" 210 | ], function (Class, TaskRegistry, Promise, scoped) { 211 | var Cls = Class.extend({scoped: scoped}, { 212 | 213 | _run: function (payload) { 214 | return jsffmpeg.ffmpeg_volume_detect(payload.source, { 215 | docker: payload.docker, 216 | timeout: payload.timeout, 217 | test_ffmpeg: payload.test_ffmpeg, 218 | test_info: payload.test_info 219 | }); 220 | } 221 | 222 | }); 223 | 224 | TaskRegistry.register("ffmpeg-volume-detect", Cls); 225 | 226 | return Cls; 227 | }); 228 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-ffmpeg", 3 | "description": "JS FFMpeg", 4 | "version": "0.0.38", 5 | "author": "Ziggeo", 6 | "repository": "https://github.com/jsonize/js-ffmpeg", 7 | "license": "Apache-2.0", 8 | "engines": { 9 | "node": "" 10 | }, 11 | "main": "index.js", 12 | "types": "types/index.d.ts", 13 | "devDependencies": { 14 | "grunt": "^1.0.1", 15 | "grunt-contrib-jshint": "^3.0.0", 16 | "grunt-shell": "^2.1.0", 17 | "qunitjs": "^2.4.1" 18 | }, 19 | "dependencies": { 20 | "betajs": "~1.0.96", 21 | "betajs-scoped": "~0.0.13", 22 | "docker-polyfill": "~0.0.1", 23 | "node-getopt": "^0.3.2", 24 | "tmp": "0.0.33" 25 | }, 26 | "scripts": { 27 | "test": "qunit tests/tests/*.js" 28 | } 29 | } -------------------------------------------------------------------------------- /src/ffmpeg-faststart.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise", 3 | "betajs:Types", 4 | "betajs:Objs" 5 | ], function(Promise, Types, Objs) { 6 | 7 | var ffmpeg = require(__dirname + "/ffmpeg.js"); 8 | var helpers = require(__dirname + "/ffmpeg-helpers.js"); 9 | module.exports = { 10 | 11 | ffmpeg_faststart: function(file, output, eventCallback, eventContext, opts) { 12 | const options = []; 13 | options.push("-c copy"); 14 | options.push(helpers.paramsFastStart); 15 | return ffmpeg.ffmpeg(file, options, output, eventCallback, eventContext, opts); 16 | } 17 | 18 | }; 19 | 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /src/ffmpeg-graceful.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise", 3 | "betajs:Types", 4 | "betajs:Objs" 5 | ], function (Promise, Types, Objs) { 6 | 7 | var ffmpeg_simple = require(__dirname + "/ffmpeg-simple.js"); 8 | var tmp = require('tmp'); 9 | 10 | module.exports = { 11 | 12 | ffmpeg_graceful: function (files, options, output, eventCallback, eventContext, opts) { 13 | options = options || {}; 14 | return ffmpeg_simple.ffmpeg_simple(files, options, output, eventCallback, eventContext, opts).mapError(function (err) { 15 | if ((Types.is_array(files) && files.length > 1) || options.output_type === 'audio' || options.output_type === "image") 16 | return err; 17 | 18 | var promise = Promise.create(); 19 | 20 | tmp.file({postfix: ".aac"}, function (err, audioFile, fd, cleanupCallback) { 21 | if (err) { 22 | promise.asyncError(err); 23 | return; 24 | } 25 | promise.callback(function () { 26 | cleanupCallback(); 27 | }); 28 | tmp.file(function (err, videoFile, fd, cleanupCallback) { 29 | if (err) { 30 | promise.asyncError(err); 31 | return; 32 | } 33 | promise.callback(function () { 34 | cleanupCallback(); 35 | }); 36 | ffmpeg_simple.ffmpeg_simple(files, {output_type: "audio"}, audioFile, eventCallback, eventContext, opts).forwardError(promise).success(function () { 37 | ffmpeg_simple.ffmpeg_simple(files, {output_type: "video", remove_audio: true }, videoFile, eventCallback, eventContext, opts).forwardError(promise).success(function () { 38 | ffmpeg_simple.ffmpeg_simple([videoFile, audioFile], options, output, eventCallback, eventContext, opts).forwardCallback(promise); 39 | }); 40 | }); 41 | }); 42 | }); 43 | 44 | return promise; 45 | }); 46 | } 47 | 48 | }; 49 | 50 | }); 51 | 52 | -------------------------------------------------------------------------------- /src/ffmpeg-helpers.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:TimeFormat" 3 | ], function (TimeFormat) { 4 | 5 | module.exports = { 6 | 7 | parseTimeCode: function (timecode) { 8 | var m = /(\d\d):(\d\d):(\d\d)\.(\d\d)/.exec(timecode); 9 | return m ? parseInt(m[1], 10) * 60 * 60 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10) + parseInt(m[4], 10) / 100 : null; 10 | }, 11 | 12 | formatTimeCode: function (seconds) { 13 | return TimeFormat.format("HH:MM:ss.L", Math.floor(seconds * 1000)); 14 | }, 15 | 16 | videoFormats: { 17 | "mp4": { 18 | bframes: true, 19 | acodec: ["libfaac", "libfdk_aac", "libvo_aacenc", "aac"], 20 | vcodec: "libx264", 21 | fmt: "mp4", 22 | passes: 2, 23 | modulus: 2, 24 | params: "-pix_fmt yuv420p" 25 | }, 26 | "mp4-av1": { 27 | bframes: true, 28 | acodec: ["aac", "libfaac", "libfdk_aac", "libvo_aacenc"], 29 | vcodec: "av1", 30 | fmt: "mp4", 31 | passes: 2, 32 | modulus: 2, 33 | params: "-strict experimental" 34 | }, 35 | "avi": { 36 | bframes: true, 37 | acodec: ["aac", "libfdk_aac", "libfaac", "libvo_aacenc"], 38 | vcodec: "libaom-av1", 39 | fmt: "avi", 40 | passes: 2, 41 | modulus: 2, 42 | params: "-strict experimental" 43 | }, 44 | "m3u8": { 45 | bframes: true, 46 | acodec: "aac", 47 | vcodec: "h264", 48 | passes: 2, 49 | modulus: 2 50 | }, 51 | "ogg": { 52 | bframes: true, 53 | acodec: "libvorbis", 54 | vcodec: "libtheora" 55 | }, 56 | "webm": { 57 | bframes: true, 58 | acodec: "libvorbis", 59 | vcodec: "libvpx", 60 | fmt: "webm" 61 | }, 62 | "wmv": { 63 | acodec: "wmav2", 64 | vcodec: "wmv2" 65 | }, 66 | "wmv3": { 67 | acodec: "wmav3", 68 | vcodec: "wmv3" 69 | }, 70 | "flv": { 71 | fmt: "flv", 72 | params: "-ar 44100" 73 | } 74 | }, 75 | 76 | paramsSynchronize: "-async 1 -metadata:s:v:0 start_time=0", 77 | 78 | paramsAudioOnly: "-vn", 79 | 80 | paramsFormatImage: "-f image2", 81 | 82 | paramsVideoMap: function (index) { return "-map " + "0:" + index; }, 83 | 84 | paramsAudioMap: function (index) { return "-map " + "1:" + index; }, 85 | 86 | paramsVideoCodecUniversalConfig: "-refs 6 -coder 1 -sc_threshold 40 -flags +loop -me_range 16 -subq 7 -i_qfactor 0.71 -qcomp 0.6 -qdiff 4 -trellis 1", 87 | 88 | paramsTimeDuration: function (time_start, time_end, time_limit) { 89 | var args = []; 90 | if (time_start) { 91 | args.push("-ss"); 92 | args.push(this.formatTimeCode(time_start)); 93 | } 94 | if (time_end) { 95 | args.push("-to"); 96 | args.push(this.formatTimeCode(time_end)); 97 | } 98 | if (time_limit) { 99 | args.push("-t"); 100 | args.push(this.formatTimeCode((time_start || 0) + time_limit)); 101 | } 102 | return args.join(" "); 103 | }, 104 | 105 | paramsFramerate: function (framerate, bframes, framerate_gop) { 106 | return "-r " + framerate + (bframes ? " -b_strategy 1 -bf 3 -g " + framerate_gop : ""); 107 | }, 108 | 109 | paramsVideoProfile: function (video_profile) { 110 | return "-profile:v " + video_profile; 111 | }, 112 | 113 | paramsFastStart: "-movflags +faststart", 114 | 115 | paramsVideoFormat: function (fmt, vcodec, acodec, params) { 116 | var args = []; 117 | if (fmt) { 118 | args.push("-f"); 119 | args.push(fmt); 120 | } 121 | if (vcodec) { 122 | args.push("-vcodec"); 123 | args.push(vcodec); 124 | } 125 | if (acodec) { 126 | args.push("-acodec"); 127 | args.push(acodec); 128 | } 129 | if (params) 130 | args.push(params); 131 | return args.join(" "); 132 | }, 133 | 134 | paramsImageExtraction: function (image_position, image_percentage, duration) { 135 | var args = []; 136 | args.push("-ss"); 137 | if (image_position !== null) 138 | args.push(this.formatTimeCode(image_position)); 139 | else if (image_percentage !== null) 140 | args.push(this.formatTimeCode(image_percentage * duration)); 141 | else 142 | args.push(this.formatTimeCode(0.5 * duration)); 143 | args.push("-vframes"); 144 | args.push("1"); 145 | return args.join(" "); 146 | }, 147 | 148 | paramsHighQualityGif: function (options) { 149 | var args = []; 150 | args.push("-filter_complex [0:v]"); 151 | if (options.framerate) 152 | args.push("fps=" + options.framerate + ","); 153 | if (options.width || options.height) 154 | args.push("scale=w=" + (options.width || -1) + ":h=" + (options.height || -1) + ":flags=lanczos,"); 155 | args.push("split[a][b];[a]palettegen[p];[b][p]paletteuse"); 156 | return args.join(""); 157 | }, 158 | 159 | parseProgress: function (progress, duration) { 160 | var raw = {}; 161 | if (progress.frame) 162 | raw.frame = parseInt(progress.frame, 10); 163 | if (progress.fps) 164 | raw.fps = parseFloat(progress.fps); 165 | if (progress.q) 166 | raw.q = parseFloat(progress.q); 167 | if (progress.size) 168 | raw.size_kb = parseInt(progress.size, 10); 169 | if (progress.bitrate) 170 | raw.bitrate_kbits = parseFloat(progress.bitrate); 171 | if (progress.dup) 172 | raw.dup = parseInt(progress.dup, 10); 173 | if (progress.drop) 174 | raw.drop = parseInt(progress.drop, 10); 175 | if (progress.time) 176 | raw.time = this.parseTimeCode(progress.time); 177 | raw.pass = progress.pass || 1; 178 | raw.passes = progress.passes || 1; 179 | if (duration && raw.time) 180 | raw.progress = (raw.pass - 1) / raw.passes + raw.time / duration / raw.passes; 181 | return raw; 182 | }, 183 | 184 | computeDuration: function (duration, time_start, time_end, time_limit) { 185 | time_end = time_end > 0 && time_end < duration ? time_end : duration; 186 | time_start = time_start > 0 ? Math.min(time_start, duration) : 0; 187 | duration = Math.max(time_end - time_start, 0); 188 | if (time_limit) 189 | duration = Math.min(duration, time_limit); 190 | return duration; 191 | } 192 | 193 | }; 194 | 195 | }); -------------------------------------------------------------------------------- /src/ffmpeg-multi-pass.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise", 3 | "betajs:Types", 4 | "betajs:Objs" 5 | ], function (Promise, Types, Objs) { 6 | 7 | var ffmpeg = require(__dirname + "/ffmpeg.js"); 8 | var tmp = require('tmp'); 9 | 10 | module.exports = { 11 | 12 | ffmpeg_multi_pass: function (files, options, passes, output, eventCallback, eventContext, opts) { 13 | options = options || []; 14 | 15 | if (passes === 1) 16 | return ffmpeg.ffmpeg(files, options, output, eventCallback, eventContext, opts); 17 | 18 | var promise = Promise.create(); 19 | 20 | tmp.file(function (err, path, fd, cleanupCallback) { 21 | if (err) { 22 | promise.asyncError(err); 23 | return; 24 | } 25 | promise.callback(function () { 26 | cleanupCallback(); 27 | }); 28 | ffmpeg.ffmpeg(files, options.concat([ 29 | '-pass', 30 | '1', 31 | '-passlogfile', 32 | path 33 | ]), output, function (progress) { 34 | progress.pass = 1; 35 | progress.passes = 2; 36 | if (eventCallback) 37 | eventCallback.call(this, progress); 38 | }, this, opts).forwardError(promise).success(function () { 39 | ffmpeg.ffmpeg(files, options.concat([ 40 | '-pass', 41 | '2', 42 | '-passlogfile', 43 | path 44 | ]), output, function (progress) { 45 | progress.pass = 2; 46 | progress.passes = 2; 47 | if (eventCallback) 48 | eventCallback.call(this, progress); 49 | }, this, opts).forwardCallback(promise); 50 | }, this); 51 | }); 52 | 53 | return promise; 54 | } 55 | 56 | }; 57 | 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /src/ffmpeg-playlist.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise", 3 | "betajs:Types", 4 | "betajs:Objs" 5 | ], function(Promise, Types, Objs) { 6 | const FS = require("fs"); 7 | 8 | var ffmpeg_multi_pass = require(__dirname + "/ffmpeg-multi-pass.js"); 9 | var ffprobe_simple = require(__dirname + "/ffprobe-simple.js"); 10 | var ffmpeg_volume_detect = require(__dirname + "/ffmpeg-volume-detect.js"); 11 | var helpers = require(__dirname + "/ffmpeg-helpers.js"); 12 | var ffmpeg_test = require(__dirname + "/ffmpeg-test.js"); 13 | 14 | module.exports = { 15 | 16 | ffmpeg_playlist: function(files, options, output, eventCallback, eventContext, opts) { 17 | return this.ffmpeg_playlist_raw(files, options, output, eventCallback, eventContext, opts).mapError(function(e) { 18 | if (e.logs) { 19 | if (e.logs.indexOf("Too many packets buffered for output stream") >= 0 && !options.maxMuxingQueueSize) { 20 | options.maxMuxingQueueSize = true; 21 | return this.ffmpeg_playlist_raw(files, options, output, eventCallback, eventContext, opts); 22 | } 23 | } 24 | return e; 25 | }, this); 26 | }, 27 | 28 | ffmpeg_playlist_raw: function(files, options, output, eventCallback, eventContext, opts) { 29 | opts = opts || {}; 30 | if (Types.is_string(files)) 31 | files = [files]; 32 | options = Objs.extend({ 33 | output_type: "video", // video, audio, image 34 | synchronize: true, 35 | framerate: 25, // null 36 | framerate_gop: 250, 37 | image_percentage: null, 38 | image_position: null, 39 | time_limit: null, 40 | time_start: 0, 41 | time_end: null, 42 | video_map: null, //0,1,2,... 43 | audio_map: null, //0,1,2 44 | video_profile: "baseline", 45 | faststart: true, 46 | video_format: "m3u8", 47 | 48 | audio_bit_rate: null, 49 | video_bit_rate: null, 50 | 51 | normalize_audio: false, 52 | remove_audio: false, 53 | width: null, 54 | height: null, 55 | auto_rotate: true, 56 | rotate: null, 57 | 58 | ratio_strategy: "fixed", // "shrink", "stretch" 59 | size_strategy: "keep", // "shrink", "stretch" 60 | shrink_strategy: "shrink-pad", // "crop", "shrink-crop" 61 | stretch_strategy: "pad", // "stretch-pad", "stretch-crop" 62 | mixed_strategy: "shrink-pad", // "stretch-crop", "crop-pad" 63 | 64 | watermark: null, 65 | watermark_size: 0.25, 66 | watermark_x: 0.95, 67 | watermark_y: 0.95, 68 | 69 | watermarks: [], 70 | 71 | maxMuxingQueueSize: false, 72 | 73 | segment_target_duration: 4, 74 | max_bitrate_ratio: 1.07, 75 | rate_monitor_buffer_ratio: 1.5, 76 | key_frames_interval: 25, 77 | renditions: [ 78 | {resolution: "640x360", bitrate: 800, audio_rate: 96}, 79 | {resolution: "842x480", bitrate: 1400, audio_rate: 128}, 80 | {resolution: "1280x720", bitrate: 2800, audio_rate: 128}, 81 | {resolution: "1920x1080", bitrate: 5000, audio_rate: 192} 82 | ] 83 | }, options); 84 | 85 | var promises = files.map(function(file) { 86 | return ffprobe_simple.ffprobe_simple(file, opts); 87 | }); 88 | 89 | if (options.watermark) { 90 | options.watermarks.unshift({ 91 | watermark: options.watermark, 92 | watermark_size: options.watermark_size, 93 | watermark_x: options.watermark_x, 94 | watermark_y: options.watermark_y 95 | }); 96 | } 97 | 98 | if (options.normalize_audio) 99 | promises.push(ffmpeg_volume_detect.ffmpeg_volume_detect(files[options.audio_map || files.length - 1], opts)); 100 | options.watermarks.forEach(function(wm) { 101 | promises.push(ffprobe_simple.ffprobe_simple(wm.watermark, opts)); 102 | }, this); 103 | if (opts.test_ffmpeg) 104 | promises.push(ffmpeg_test.ffmpeg_test(opts)); 105 | 106 | return Promise.and(promises).mapSuccess(function(infos) { 107 | 108 | var testInfo = opts.test_info || {}; 109 | if (opts.test_ffmpeg) 110 | testInfo = infos.pop(); 111 | 112 | var watermarkInfos = []; 113 | options.watermarks.forEach(function() { 114 | watermarkInfos.unshift(infos.pop()); 115 | }); 116 | 117 | var audioNormalizationInfo = null; 118 | if (options.normalize_audio) 119 | audioNormalizationInfo = infos.pop(); 120 | 121 | var isImage = infos.length === 1 && infos[0].image && !infos[0].video && !infos[0].audio; 122 | 123 | var passes = 1; 124 | 125 | var args = []; 126 | 127 | /* 128 | * 129 | * Synchronize Audio & Video 130 | * 131 | */ 132 | if (options.remove_audio) { 133 | args.push("-an"); 134 | } else if (options.synchronize) { 135 | args.push(helpers.paramsSynchronize); 136 | } 137 | 138 | /* 139 | * 140 | * Map Streams 141 | * 142 | */ 143 | if (infos.length > 1) { 144 | var videoIdx = options.video_map || 0; 145 | args.push("-map " + videoIdx + ":" + infos[videoIdx].video.index); 146 | } 147 | if (infos.length > 1) { 148 | var audioIdx = options.audio_map || 1; 149 | args.push("-map " + audioIdx + ":" + infos[audioIdx].audio.index); 150 | } 151 | 152 | /* 153 | * 154 | * Audio Normalization? 155 | * 156 | */ 157 | if (audioNormalizationInfo) { 158 | args.push("-af"); 159 | args.push("volume=" + (-audioNormalizationInfo.max_volume) + "dB"); 160 | } 161 | 162 | /* 163 | * 164 | * Which time region should be used? 165 | * 166 | */ 167 | var duration = helpers.computeDuration(infos[0].duration, options.time_start, options.time_end, 168 | options.time_limit); 169 | if (options.time_start || options.time_end || options.time_limit) 170 | args.push(helpers.paramsTimeDuration(options.time_start, options.time_end, options.time_limit)); 171 | 172 | var videoInfo = infos[0].video; 173 | var audioInfo = infos[1] ? infos[1].audio || infos[0].audio : infos[0].audio; 174 | 175 | var sourceWidth = 0; 176 | var sourceHeight = 0; 177 | var targetWidth = 0; 178 | var targetHeight = 0; 179 | //try { 180 | 181 | var source = infos[0]; 182 | var sourceInfo = source.video || source.image; 183 | var requiredRotation = 0; 184 | if (options.auto_rotate && !(testInfo.capabilities && testInfo.capabilities.auto_rotate)) 185 | requiredRotation = sourceInfo.rotation % 360; 186 | if (options.rotate) { 187 | requiredRotation = (requiredRotation + options.rotate) % 360; 188 | if (options.rotate % 180 === 90) { 189 | var temp = sourceInfo.rotated_width; 190 | sourceInfo.rotated_width = sourceInfo.rotated_height; 191 | sourceInfo.rotated_height = temp; 192 | } 193 | } 194 | sourceWidth = sourceInfo.rotated_width; 195 | sourceHeight = sourceInfo.rotated_height; 196 | var sourceRatio = sourceWidth / sourceHeight; 197 | targetWidth = sourceWidth; 198 | targetHeight = sourceHeight; 199 | var targetRatio = sourceRatio; 200 | var ratioSourceTarget = 0; 201 | 202 | /* 203 | * 204 | * Which sizing should be used? 205 | * 206 | */ 207 | 208 | var renditionArgs = {}; 209 | Objs.iter(options.renditions, function(rendition, i) { 210 | // Step 1: Fix Rotation 211 | renditionArgs[rendition.resolution] = {}; 212 | var vfilters = []; 213 | var sizing = ""; 214 | 215 | if (requiredRotation !== 0) { 216 | if (requiredRotation % 180 === 90) { 217 | vfilters.push("transpose=" + (requiredRotation === 90 ? 1 : 2)); 218 | } 219 | if (requiredRotation === 180) { 220 | vfilters.push("hflip,vflip"); 221 | } 222 | args.push("-metadata:s:v:0"); 223 | args.push("rotate=0"); 224 | } 225 | 226 | let widthHeight = rendition.resolution.split("x"); 227 | let renditionWidth = Types.parseInt(widthHeight[0]); 228 | let renditionHeight = Types.parseInt(widthHeight[1]); 229 | 230 | var modulus = options.output_type === "video" ? helpers.videoFormats[options.video_format].modulus || 1 : 1; 231 | var modulusAdjust = function(value) { 232 | value = Math.round(value); 233 | return value % modulus === 0 ? value : (Math.round(value / modulus) * modulus); 234 | }; 235 | 236 | if (modulusAdjust(sourceWidth) !== sourceWidth || modulusAdjust(sourceHeight) !== sourceHeight || 237 | renditionWidth || renditionHeight) { 238 | 239 | // Step 2: Fix Size & Ratio 240 | targetWidth = renditionWidth || sourceWidth; 241 | targetHeight = renditionHeight || sourceHeight; 242 | targetRatio = targetWidth / targetHeight; 243 | ratioSourceTarget = Math.sign(sourceWidth * targetHeight - targetWidth * sourceHeight); 244 | 245 | if (options.ratio_strategy !== "fixed" && ratioSourceTarget !== 0) { 246 | if ((options.ratio_strategy === "stretch" && ratioSourceTarget > 0) || 247 | (options.ratio_strategy === "shrink" && ratioSourceTarget < 0)) 248 | targetWidth = targetHeight * sourceRatio; 249 | if ((options.ratio_strategy === "stretch" && ratioSourceTarget < 0) || 250 | (options.ratio_strategy === "shrink" && ratioSourceTarget > 0)) 251 | targetHeight = targetWidth / sourceRatio; 252 | targetRatio = sourceRatio; 253 | ratioSourceTarget = 0; 254 | } 255 | 256 | if (options.size_strategy === "shrink" && targetWidth > sourceWidth && targetHeight > sourceHeight) { 257 | targetWidth = ratioSourceTarget < 0 ? sourceHeight * targetRatio : sourceWidth; 258 | targetHeight = ratioSourceTarget >= 0 ? targetWidth / targetRatio : sourceHeight; 259 | } else if (options.size_strategy === "stretch" && targetWidth < sourceWidth && targetHeight < 260 | sourceHeight) { 261 | targetWidth = ratioSourceTarget >= 0 ? sourceHeight * targetRatio : sourceWidth; 262 | targetHeight = ratioSourceTarget < 0 ? targetWidth / targetRatio : sourceHeight; 263 | } 264 | 265 | var vf = []; 266 | 267 | // Step 3: Modulus 268 | 269 | targetWidth = modulusAdjust(targetWidth); 270 | targetHeight = modulusAdjust(targetHeight); 271 | 272 | var cropped = false; 273 | var addCrop = function(x, y, multi) { 274 | x = Math.round(x); 275 | y = Math.round(y); 276 | if (x === 0 && y === 0) 277 | return; 278 | cropped = true; 279 | var cropWidth = targetWidth - 2 * x; 280 | var cropHeight = targetHeight - 2 * y; 281 | vfilters.push("scale=" + [ 282 | multi || ratioSourceTarget >= 0 ? cropWidth : targetWidth, 283 | !multi && ratioSourceTarget >= 0 ? targetHeight : cropHeight].join(":")); 284 | vfilters.push("crop=" + [ 285 | !multi && ratioSourceTarget <= 0 ? cropWidth : targetWidth, 286 | multi || ratioSourceTarget <= 0 ? targetHeight : cropHeight, 287 | -x, 288 | -y].join(":")); 289 | }; 290 | var padded = false; 291 | var addPad = function(x, y, multi) { 292 | x = Math.round(x); 293 | y = Math.round(y); 294 | if (x === 0 && y === 0) 295 | return; 296 | padded = true; 297 | var padWidth = targetWidth - 2 * x; 298 | var padHeight = targetHeight - 2 * y; 299 | vfilters.push("scale=" + [ 300 | multi || ratioSourceTarget <= 0 ? padWidth : targetWidth, 301 | !multi && ratioSourceTarget <= 0 ? targetHeight : padHeight].join(":")); 302 | vfilters.push("pad=" + [ 303 | !multi && ratioSourceTarget >= 0 ? padWidth : targetWidth, 304 | multi || ratioSourceTarget >= 0 ? targetHeight : padHeight, 305 | x, 306 | y].join(":")); 307 | }; 308 | 309 | // Step 4: Crop & Pad 310 | if (targetWidth >= sourceWidth && targetHeight >= sourceHeight) { 311 | if (options.stretch_strategy === "pad") 312 | addPad((targetWidth - sourceWidth) / 2, 313 | (targetHeight - sourceHeight) / 2, 314 | true); 315 | else if (options.stretch_strategy === "stretch-pad") 316 | addPad(ratioSourceTarget <= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 317 | ratioSourceTarget >= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 318 | else // stretch-crop 319 | addCrop(ratioSourceTarget >= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 320 | ratioSourceTarget <= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 321 | } else if (targetWidth <= sourceWidth && targetHeight <= sourceHeight) { 322 | if (options.shrink_strategy === "crop") 323 | addCrop((targetWidth - sourceWidth) / 2, 324 | (targetHeight - sourceHeight) / 2, 325 | true); 326 | else if (options.shrink_strategy === "shrink-crop") 327 | addCrop(ratioSourceTarget >= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 328 | ratioSourceTarget <= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 329 | else // shrink-pad 330 | addPad(ratioSourceTarget <= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 331 | ratioSourceTarget >= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 332 | } else { 333 | if (options.mixed_strategy === "shrink-pad") 334 | addPad(ratioSourceTarget <= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 335 | ratioSourceTarget >= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 336 | else if (options.mixed_strategy === "stretch-crop") 337 | addCrop(ratioSourceTarget >= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 338 | ratioSourceTarget <= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 339 | else { 340 | // crop-pad 341 | cropped = true; 342 | padded = true; 343 | var direction = ratioSourceTarget >= 0; 344 | var dirX = Math.abs(Math.round((sourceWidth - targetWidth) / 2)); 345 | var dirY = Math.abs(Math.round((sourceHeight - targetHeight) / 2)); 346 | vfilters.push("crop=" + [ 347 | direction ? targetWidth : sourceWidth, 348 | direction ? sourceHeight : targetHeight, 349 | direction ? dirX : 0, 350 | direction ? 0 : dirY].join(":")); 351 | vfilters.push( 352 | "pad=" + [targetWidth, targetHeight, direction ? 0 : dirX, direction ? dirY : 0].join(":")); 353 | } 354 | } 355 | 356 | if (!padded && !cropped) 357 | renditionArgs[rendition.resolution].sizing = targetWidth + "x" + targetHeight; 358 | 359 | } 360 | 361 | vfilters = vfilters.join(","); 362 | 363 | /* 364 | * 365 | * Watermark (depends on sizing) 366 | * 367 | */ 368 | 369 | var watermarkFilters = options.watermarks.map(function(watermark, i) { 370 | var watermarkInfo = watermarkInfos[i]; 371 | var watermarkMeta = watermarkInfo.image || watermarkInfo.video; 372 | var scaleWidth = watermarkMeta.width; 373 | var scaleHeight = watermarkMeta.height; 374 | var maxWidth = targetWidth * watermark.watermark_size; 375 | var maxHeight = targetHeight * watermark.watermark_size; 376 | if (scaleWidth > maxWidth || scaleHeight > maxHeight) { 377 | var watermarkRatio = maxWidth * scaleHeight >= maxHeight * scaleWidth; 378 | scaleWidth = watermarkRatio ? watermarkMeta.width * maxHeight / watermarkMeta.height : maxWidth; 379 | scaleHeight = !watermarkRatio ? watermarkMeta.height * maxWidth / watermarkMeta.width : maxHeight; 380 | } 381 | var posX = watermark.watermark_x * (targetWidth - scaleWidth); 382 | var posY = watermark.watermark_y * (targetHeight - scaleHeight); 383 | 384 | return [ 385 | "[prewm" + i + "];", 386 | "movie=" + watermark.watermark + ",", 387 | "scale=" + [Math.round(scaleWidth), Math.round(scaleHeight)].join(":"), 388 | "[wm" + i + "];", 389 | "[prewm" + i + "][wm" + i + "]", 390 | "overlay=" + [Math.round(posX), Math.round(posY)].join(":") 391 | ].join(""); 392 | }).join(""); 393 | 394 | if (watermarkFilters) { 395 | if (vfilters) 396 | vfilters = "[in]" + vfilters + watermarkFilters + "[out]"; 397 | else 398 | vfilters = watermarkFilters.substring("[prewm0];".length).replace("[prewm0]", "[in]") + "[out]"; 399 | } 400 | 401 | renditionArgs[rendition.resolution].vf = vfilters; 402 | 403 | }); 404 | /* 405 | * 406 | * Format 407 | * 408 | */ 409 | 410 | if (options.video_profile && options.video_format === "mp4") { 411 | args.push(helpers.paramsVideoProfile(options.video_profile)); 412 | } 413 | if (options.faststart && options.video_format === "mp4") { 414 | args.push(helpers.paramsFastStart); 415 | } 416 | var format = helpers.videoFormats[options.video_format]; 417 | if (format && (format.fmt || format.vcodec || format.acodec || format.params)) { 418 | var acodec = format.acodec; 419 | if (Types.is_array(acodec)) { 420 | if (testInfo.encoders) { 421 | var encoders = Objs.objectify(testInfo.encoders); 422 | acodec = acodec.filter(function(codec) { 423 | return encoders[codec]; 424 | }); 425 | } 426 | if (acodec.length === 0) 427 | acodec = format.acodec; 428 | acodec = acodec[0]; 429 | } 430 | args.push(helpers.paramsVideoFormat(format.fmt, format.vcodec, acodec, format.params)); 431 | } 432 | args.push(...helpers.paramsVideoCodecUniversalConfig.split(" ")); 433 | if (format && format.passes > 1) 434 | passes = format.passes; 435 | 436 | // Workaround for https://trac.ffmpeg.org/ticket/6375 437 | if (options.maxMuxingQueueSize) { 438 | args.push("-max_muxing_queue_size"); 439 | args.push("9999"); 440 | } 441 | 442 | let newArgs = []; 443 | const target = output; 444 | if (!FS.existsSync(target)) 445 | FS.mkdirSync(target, {recursive: true}); 446 | let masterPlaylist = "#EXTM3U\n#EXT-X-VERSION:3\n"; 447 | const keyFramesInterval = options.key_frames_interval; 448 | let staticParams = `-g ${keyFramesInterval} -keyint_min ${keyFramesInterval} -hls_time ${options.segment_target_duration}`; 449 | staticParams += ` -hls_playlist_type vod`; 450 | Objs.iter(options.renditions, function(obj, i) { 451 | let resolution = renditionArgs[obj.resolution].sizing ? renditionArgs[obj.resolution].sizing : obj.resolution; 452 | let widthHeight = resolution.split("x"); 453 | let width = Types.parseInt(widthHeight[0]); 454 | let height = Types.parseInt(widthHeight[1]); 455 | let maxRate = obj.bitrate * options.max_bitrate_ratio; 456 | let bufSize = obj.bitrate * options.rate_monitor_buffer_ratio; 457 | let bandwidth = obj.bitrate * 1000; 458 | let name = `${height}p`; 459 | newArgs.push(...args); 460 | newArgs.push(...staticParams.split(" ")); 461 | if (renditionArgs[obj.resolution].vf) { 462 | newArgs.push(...`-vf ${renditionArgs[obj.resolution].vf}`.split(" ")); 463 | } else { 464 | newArgs.push(...`-vf scale=w=${width}:h=${height}:force_original_aspect_ratio=decrease`.split(" ")); 465 | } 466 | newArgs.push( 467 | ...`-b:v ${obj.bitrate}k -maxrate ${maxRate}k -bufsize ${bufSize}k -b:a ${obj.audio_rate}k`.split(" ")); 468 | newArgs.push(...`-hls_segment_filename ${target}/${name}_%03d.ts ${target}/${name}.m3u8`.split(" ")); 469 | masterPlaylist += `#EXT-X-STREAM-INF:BANDWIDTH=${bandwidth},RESOLUTION=${resolution}\n${name}.m3u8\n`; 470 | }); 471 | return ffmpeg_multi_pass.ffmpeg_multi_pass(files, newArgs, passes, null, function(progress) { 472 | if (eventCallback) 473 | eventCallback.call(eventContext || this, helpers.parseProgress(progress, duration)); 474 | }, this, opts).mapSuccess(function() { 475 | FS.writeFileSync(target + "/playlist.m3u8", masterPlaylist); 476 | return Promise.value({playlist: target + "/playlist.m3u8"}); 477 | }, this); 478 | }); 479 | 480 | } 481 | 482 | }; 483 | 484 | }); 485 | 486 | -------------------------------------------------------------------------------- /src/ffmpeg-simple.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise", 3 | "betajs:Types", 4 | "betajs:Objs" 5 | ], function (Promise, Types, Objs) { 6 | 7 | var ffmpeg_multi_pass = require(__dirname + "/ffmpeg-multi-pass.js"); 8 | var ffprobe_simple = require(__dirname + "/ffprobe-simple.js"); 9 | var ffmpeg_volume_detect = require(__dirname + "/ffmpeg-volume-detect.js"); 10 | var helpers = require(__dirname + "/ffmpeg-helpers.js"); 11 | var ffmpeg_test = require(__dirname + "/ffmpeg-test.js"); 12 | 13 | 14 | module.exports = { 15 | 16 | ffmpeg_simple: function (files, options, output, eventCallback, eventContext, opts) { 17 | return this.ffmpeg_simple_raw(files, options, output, eventCallback, eventContext, opts).mapError(function (e) { 18 | if (e.logs) { 19 | if (e.logs.indexOf("Too many packets buffered for output stream") >= 0 && !options.maxMuxingQueueSize) { 20 | options.maxMuxingQueueSize = true; 21 | return this.ffmpeg_simple_raw(files, options, output, eventCallback, eventContext, opts); 22 | } 23 | } 24 | return e; 25 | }, this); 26 | }, 27 | 28 | ffmpeg_simple_raw: function (files, options, output, eventCallback, eventContext, opts) { 29 | opts = opts || {}; 30 | if (Types.is_string(files)) 31 | files = [files]; 32 | options = Objs.extend({ 33 | output_type: "video", // video, audio, image 34 | synchronize: true, 35 | framerate: 25, // null 36 | framerate_gop: 250, 37 | image_percentage: null, 38 | image_position: null, 39 | time_limit: null, 40 | time_start: 0, 41 | time_end: null, 42 | video_map: null, //0,1,2,... 43 | audio_map: null, //0,1,2 44 | video_profile: "baseline", 45 | faststart: true, 46 | video_format: "mp4", 47 | 48 | audio_bit_rate: null, 49 | video_bit_rate: null, 50 | 51 | normalize_audio: false, 52 | remove_audio: false, 53 | width: null, 54 | height: null, 55 | auto_rotate: true, 56 | rotate: null, 57 | 58 | ratio_strategy: "fixed", // "shrink", "stretch" 59 | size_strategy: "keep", // "shrink", "stretch" 60 | shrink_strategy: "shrink-pad", // "crop", "shrink-crop" 61 | stretch_strategy: "pad", // "stretch-pad", "stretch-crop" 62 | mixed_strategy: "shrink-pad", // "stretch-crop", "crop-pad" 63 | 64 | watermark: null, 65 | watermark_size: 0.25, 66 | watermark_x: 0.95, 67 | watermark_y: 0.95, 68 | 69 | watermarks: [], 70 | 71 | maxMuxingQueueSize: false 72 | }, options); 73 | 74 | var promises = files.map(function (file) { 75 | return ffprobe_simple.ffprobe_simple(file, opts); 76 | }); 77 | 78 | if (options.watermark) { 79 | options.watermarks.unshift({ 80 | watermark: options.watermark, 81 | watermark_size: options.watermark_size, 82 | watermark_x: options.watermark_x, 83 | watermark_y: options.watermark_y 84 | }); 85 | } 86 | 87 | if (options.normalize_audio) 88 | promises.push(ffmpeg_volume_detect.ffmpeg_volume_detect(files[options.audio_map || files.length - 1], opts)); 89 | options.watermarks.forEach(function (wm) { 90 | promises.push(ffprobe_simple.ffprobe_simple(wm.watermark, opts)); 91 | }, this); 92 | if (opts.test_ffmpeg) 93 | promises.push(ffmpeg_test.ffmpeg_test(opts)); 94 | 95 | return Promise.and(promises).mapSuccess(function (infos) { 96 | 97 | var testInfo = opts.test_info || {}; 98 | if (opts.test_ffmpeg) 99 | testInfo = infos.pop(); 100 | 101 | var watermarkInfos = []; 102 | options.watermarks.forEach(function () { 103 | watermarkInfos.unshift(infos.pop()); 104 | }); 105 | 106 | var audioNormalizationInfo = null; 107 | if (options.normalize_audio) 108 | audioNormalizationInfo = infos.pop(); 109 | 110 | var isImage = infos.length === 1 && ((infos[0].image && !infos[0].video && !infos[0].audio) || 111 | (infos[0].video && infos[0].format_default_extension.includes("_pipe"))); 112 | 113 | var passes = 1; 114 | 115 | var args = []; 116 | 117 | 118 | /* 119 | * 120 | * Synchronize Audio & Video 121 | * 122 | */ 123 | if (options.output_type === 'video') { 124 | if (options.remove_audio) 125 | args.push("-an"); 126 | else if (options.synchronize) 127 | args.push(helpers.paramsSynchronize); 128 | } 129 | 130 | 131 | /* 132 | * 133 | * Map Streams 134 | * 135 | */ 136 | if (options.output_type === 'audio') { 137 | args.push(helpers.paramsAudioOnly); 138 | } else if (options.output_type === 'video') { 139 | if (infos.length > 1) { 140 | var videoIdx = options.video_map || 0; 141 | args.push("-map " + videoIdx + ":" + infos[videoIdx].video.index); 142 | } 143 | if (infos.length > 1) { 144 | var audioIdx = options.audio_map || 1; 145 | args.push("-map " + audioIdx + ":" + infos[audioIdx].audio.index); 146 | } 147 | } 148 | 149 | /* 150 | * 151 | * Audio Normalization? 152 | * 153 | */ 154 | if (audioNormalizationInfo) { 155 | args.push("-af"); 156 | args.push("volume=" + (-audioNormalizationInfo.max_volume) + "dB"); 157 | } 158 | 159 | 160 | /* 161 | * 162 | * Which time region should be used? 163 | * 164 | */ 165 | var duration = helpers.computeDuration(infos[0].duration, options.time_start, options.time_end, options.time_limit); 166 | if (!isImage) { 167 | if (options.output_type === 'image') 168 | args.push(helpers.paramsImageExtraction(options.image_position, options.image_percentage, duration)); 169 | else if (options.time_start || options.time_end || options.time_limit) 170 | args.push(helpers.paramsTimeDuration(options.time_start, options.time_end, options.time_limit)); 171 | } 172 | 173 | 174 | var videoInfo = infos[0].video; 175 | if (videoInfo && infos[0].bit_rate && (!videoInfo.bit_rate || infos[0].bit_rate > videoInfo.bit_rate)) 176 | videoInfo.bit_rate = infos[0].bit_rate; 177 | 178 | var audioInfo = infos[1] ? infos[1].audio || infos[0].audio : infos[0].audio; 179 | 180 | 181 | var sourceWidth = 0; 182 | var sourceHeight = 0; 183 | var targetWidth = 0; 184 | var targetHeight = 0; 185 | //try { 186 | 187 | if (options.output_type !== 'audio') { 188 | var source = infos[0]; 189 | var sourceInfo = source.video || source.image; 190 | var requiredRotation = 0; 191 | if (options.auto_rotate && !(testInfo.capabilities && testInfo.capabilities.auto_rotate)) 192 | requiredRotation = sourceInfo.rotation % 360; 193 | if (options.rotate) { 194 | requiredRotation = (requiredRotation + options.rotate) % 360; 195 | if (options.rotate % 180 === 90) { 196 | var temp = sourceInfo.rotated_width; 197 | sourceInfo.rotated_width = sourceInfo.rotated_height; 198 | sourceInfo.rotated_height = temp; 199 | } 200 | } 201 | sourceWidth = sourceInfo.rotated_width; 202 | sourceHeight = sourceInfo.rotated_height; 203 | var sourceRatio = sourceWidth / sourceHeight; 204 | targetWidth = sourceWidth; 205 | targetHeight = sourceHeight; 206 | var targetRatio = sourceRatio; 207 | var ratioSourceTarget = 0; 208 | 209 | /* 210 | * 211 | * Which sizing should be used? 212 | * 213 | */ 214 | 215 | // Step 1: Fix Rotation 216 | var vfilters = []; 217 | var sizing = ""; 218 | 219 | if (requiredRotation !== 0) { 220 | if (requiredRotation % 180 === 90) { 221 | vfilters.push("transpose=" + (requiredRotation === 90 ? 1 : 2)); 222 | } 223 | if (requiredRotation === 180) { 224 | vfilters.push("hflip,vflip"); 225 | } 226 | args.push("-metadata:s:v:0"); 227 | args.push("rotate=0"); 228 | } 229 | 230 | var modulus = options.output_type === 'video' ? helpers.videoFormats[options.video_format].modulus || 1 : 1; 231 | var modulusAdjust = function (value) { 232 | value = Math.round(value); 233 | return value % modulus === 0 ? value : (Math.round(value / modulus) * modulus); 234 | }; 235 | 236 | if (modulusAdjust(sourceWidth) !== sourceWidth || modulusAdjust(sourceHeight) !== sourceHeight || options.width || options.height) { 237 | 238 | // Step 2: Fix Size & Ratio 239 | targetWidth = options.width || sourceWidth; 240 | targetHeight = options.height || sourceHeight; 241 | targetRatio = targetWidth / targetHeight; 242 | ratioSourceTarget = Math.sign(sourceWidth * targetHeight - targetWidth * sourceHeight); 243 | 244 | if (options.ratio_strategy !== "fixed" && ratioSourceTarget !== 0) { 245 | if ((options.ratio_strategy === "stretch" && ratioSourceTarget > 0) || (options.ratio_strategy === "shrink" && ratioSourceTarget < 0)) 246 | targetWidth = targetHeight * sourceRatio; 247 | if ((options.ratio_strategy === "stretch" && ratioSourceTarget < 0) || (options.ratio_strategy === "shrink" && ratioSourceTarget > 0)) 248 | targetHeight = targetWidth / sourceRatio; 249 | targetRatio = sourceRatio; 250 | ratioSourceTarget = 0; 251 | } 252 | 253 | if (options.size_strategy === "shrink" && targetWidth > sourceWidth && targetHeight > sourceHeight) { 254 | targetWidth = ratioSourceTarget < 0 ? sourceHeight * targetRatio : sourceWidth; 255 | targetHeight = ratioSourceTarget >= 0 ? targetWidth / targetRatio : sourceHeight; 256 | } else if (options.size_strategy === "stretch" && targetWidth < sourceWidth && targetHeight < sourceHeight) { 257 | targetWidth = ratioSourceTarget >= 0 ? sourceHeight * targetRatio : sourceWidth; 258 | targetHeight = ratioSourceTarget < 0 ? targetWidth / targetRatio : sourceHeight; 259 | } 260 | 261 | var vf = []; 262 | 263 | // Step 3: Modulus 264 | 265 | targetWidth = modulusAdjust(targetWidth); 266 | targetHeight = modulusAdjust(targetHeight); 267 | 268 | var cropped = false; 269 | var addCrop = function (x, y, multi) { 270 | x = Math.round(x); 271 | y = Math.round(y); 272 | if (x === 0 && y === 0) 273 | return; 274 | cropped = true; 275 | var cropWidth = targetWidth - 2 * x; 276 | var cropHeight = targetHeight - 2 * y; 277 | vfilters.push("scale=" + [multi || ratioSourceTarget >= 0 ? cropWidth : targetWidth, !multi && ratioSourceTarget >= 0 ? targetHeight : cropHeight].join(":")); 278 | vfilters.push("crop=" + [!multi && ratioSourceTarget <= 0 ? cropWidth : targetWidth, multi || ratioSourceTarget <= 0 ? targetHeight : cropHeight, -x, -y].join(":")); 279 | }; 280 | var padded = false; 281 | var addPad = function (x, y, multi) { 282 | x = Math.round(x); 283 | y = Math.round(y); 284 | if (x === 0 && y === 0) 285 | return; 286 | padded = true; 287 | var padWidth = targetWidth - 2 * x; 288 | var padHeight = targetHeight - 2 * y; 289 | vfilters.push("scale=" + [multi || ratioSourceTarget <= 0 ? padWidth : targetWidth, !multi && ratioSourceTarget <= 0 ? targetHeight : padHeight].join(":")); 290 | vfilters.push("pad=" + [!multi && ratioSourceTarget >= 0 ? padWidth : targetWidth, multi || ratioSourceTarget >= 0 ? targetHeight : padHeight, x, y].join(":")); 291 | }; 292 | 293 | // Step 4: Crop & Pad 294 | if (targetWidth >= sourceWidth && targetHeight >= sourceHeight) { 295 | if (options.stretch_strategy === "pad") 296 | addPad((targetWidth - sourceWidth) / 2, 297 | (targetHeight - sourceHeight) / 2, 298 | true); 299 | else if (options.stretch_strategy === "stretch-pad") 300 | addPad(ratioSourceTarget <= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 301 | ratioSourceTarget >= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 302 | else // stretch-crop 303 | addCrop(ratioSourceTarget >= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 304 | ratioSourceTarget <= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 305 | } else if (targetWidth <= sourceWidth && targetHeight <= sourceHeight) { 306 | if (options.shrink_strategy === "crop") 307 | addCrop((targetWidth - sourceWidth) / 2, 308 | (targetHeight - sourceHeight) / 2, 309 | true); 310 | else if (options.shrink_strategy === "shrink-crop") 311 | addCrop(ratioSourceTarget >= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 312 | ratioSourceTarget <= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 313 | else // shrink-pad 314 | addPad(ratioSourceTarget <= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 315 | ratioSourceTarget >= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 316 | } else { 317 | if (options.mixed_strategy === "shrink-pad") 318 | addPad(ratioSourceTarget <= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 319 | ratioSourceTarget >= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 320 | else if (options.mixed_strategy === "stretch-crop") 321 | addCrop(ratioSourceTarget >= 0 ? (targetWidth - targetHeight * sourceRatio) / 2 : 0, 322 | ratioSourceTarget <= 0 ? (targetHeight - targetWidth / sourceRatio) / 2 : 0); 323 | else { 324 | // crop-pad 325 | cropped = true; 326 | padded = true; 327 | var direction = ratioSourceTarget >= 0; 328 | var dirX = Math.abs(Math.round((sourceWidth - targetWidth) / 2)); 329 | var dirY = Math.abs(Math.round((sourceHeight - targetHeight) / 2)); 330 | vfilters.push("crop=" + [direction ? targetWidth : sourceWidth, direction ? sourceHeight : targetHeight, direction ? dirX : 0, direction ? 0 : dirY].join(":")); 331 | vfilters.push("pad=" + [targetWidth, targetHeight, direction ? 0 : dirX, direction ? dirY : 0].join(":")); 332 | } 333 | } 334 | 335 | if (!padded && !cropped) 336 | sizing = targetWidth + "x" + targetHeight; 337 | 338 | } 339 | 340 | vfilters = vfilters.join(","); 341 | 342 | /* 343 | * 344 | * Watermark (depends on sizing) 345 | * 346 | */ 347 | 348 | var watermarkFilters = options.watermarks.map(function (watermark, i) { 349 | var watermarkInfo = watermarkInfos[i]; 350 | var watermarkMeta = watermarkInfo.image || watermarkInfo.video; 351 | var scaleWidth = watermarkMeta.width; 352 | var scaleHeight = watermarkMeta.height; 353 | var maxWidth = targetWidth * watermark.watermark_size; 354 | var maxHeight = targetHeight * watermark.watermark_size; 355 | if (scaleWidth > maxWidth || scaleHeight > maxHeight) { 356 | var watermarkRatio = maxWidth * scaleHeight >= maxHeight * scaleWidth; 357 | scaleWidth = watermarkRatio ? watermarkMeta.width * maxHeight / watermarkMeta.height : maxWidth; 358 | scaleHeight = !watermarkRatio ? watermarkMeta.height * maxWidth / watermarkMeta.width : maxHeight; 359 | } 360 | var posX = watermark.watermark_x * (targetWidth - scaleWidth); 361 | var posY = watermark.watermark_y * (targetHeight - scaleHeight); 362 | 363 | 364 | return [ 365 | "[prewm" + i + "];", 366 | "movie=" + watermark.watermark + ",", 367 | "scale=" + [Math.round(scaleWidth), Math.round(scaleHeight)].join(":"), 368 | "[wm" + i + "];", 369 | "[prewm" + i + "][wm" + i + "]", 370 | "overlay=" + [Math.round(posX), Math.round(posY)].join(":") 371 | ].join(""); 372 | }).join(""); 373 | 374 | if (watermarkFilters) { 375 | if (vfilters) 376 | vfilters = "[in]" + vfilters + watermarkFilters + "[out]"; 377 | else 378 | vfilters = watermarkFilters.substring("[prewm0];".length).replace("[prewm0]", "[in]") + "[out]"; 379 | } 380 | 381 | 382 | // Video Filters 383 | if (vfilters && options.output_type !== "gif") { 384 | args.push("-vf"); 385 | args.push(vfilters); 386 | } 387 | if (sizing) { 388 | args.push("-s"); 389 | args.push(sizing); 390 | } 391 | 392 | 393 | } 394 | 395 | 396 | /* 397 | * 398 | * Format 399 | * 400 | */ 401 | if (options.output_type === 'image') 402 | args.push(helpers.paramsFormatImage); 403 | if (options.output_type === 'video') { 404 | if (options.video_profile && options.video_format === "mp4") 405 | args.push(helpers.paramsVideoProfile(options.video_profile)); 406 | if (options.faststart && options.video_format === "mp4") 407 | args.push(helpers.paramsFastStart); 408 | var format = helpers.videoFormats[options.video_format]; 409 | if (format && (format.fmt || format.vcodec || format.acodec || format.params)) { 410 | var acodec = format.acodec; 411 | if (Types.is_array(acodec)) { 412 | if (testInfo.encoders) { 413 | var encoders = Objs.objectify(testInfo.encoders); 414 | acodec = acodec.filter(function (codec) { 415 | return encoders[codec]; 416 | }); 417 | } 418 | if (acodec.length === 0) 419 | acodec = format.acodec; 420 | acodec = acodec[0]; 421 | } 422 | args.push(helpers.paramsVideoFormat(format.fmt, format.vcodec, acodec, format.params)); 423 | } 424 | if (options.framerate) 425 | args.push(helpers.paramsFramerate(options.framerate, format.bframes, options.framerate_gop)); 426 | args.push(helpers.paramsVideoCodecUniversalConfig); 427 | if (format && format.passes > 1) 428 | passes = format.passes; 429 | } 430 | if (options.output_type === "gif") { 431 | args.push(helpers.paramsHighQualityGif({ 432 | "width": options.width, 433 | "height": options.height, 434 | "framerate": options.framerate 435 | })); 436 | } 437 | 438 | 439 | /* 440 | * 441 | * Bit rate (depends on watermark + sizing) 442 | * 443 | */ 444 | if (options.output_type === "video") { 445 | args.push("-b:v"); 446 | var video_bit_rate = options.video_bit_rate; 447 | if (!video_bit_rate && videoInfo.bit_rate) 448 | video_bit_rate = videoInfo.bit_rate * Math.min(Math.max(targetWidth * targetHeight / sourceWidth / sourceHeight, 0.25), 4); 449 | if (!video_bit_rate) 450 | video_bit_rate = Math.round(1000 * (targetWidth + targetHeight) / 2); 451 | args.push(Math.round(video_bit_rate / 1000) + "k"); 452 | if (audioInfo) { 453 | args.push("-b:a"); 454 | var audio_bit_rate = options.audio_bit_rate || Math.max(audioInfo.bit_rate || 64000, 64000); 455 | args.push(Math.round(audio_bit_rate / 1000) + "k"); 456 | } 457 | } 458 | 459 | // Workaround for https://trac.ffmpeg.org/ticket/6375 460 | if (options.maxMuxingQueueSize) { 461 | args.push("-max_muxing_queue_size"); 462 | args.push("9999"); 463 | } 464 | 465 | //} catch(e) {console.log(e);} 466 | 467 | //console.log(files, args, passes, output); 468 | return ffmpeg_multi_pass.ffmpeg_multi_pass(files, args, passes, output, function (progress) { 469 | if (eventCallback) 470 | eventCallback.call(eventContext || this, helpers.parseProgress(progress, duration)); 471 | }, this, opts).mapSuccess(function () { 472 | return ffprobe_simple.ffprobe_simple(output, opts); 473 | }, this); 474 | }); 475 | 476 | 477 | } 478 | 479 | }; 480 | 481 | }); 482 | 483 | -------------------------------------------------------------------------------- /src/ffmpeg-test.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise" 3 | ], function (Promise) { 4 | 5 | var DockerPolyfill = require("docker-polyfill"); 6 | 7 | var VersionRegex = /^ffmpeg version (\d+)\.(\d+)\.(\d+).*$/i; 8 | var ConfigurationRegex = /^\s+configuration:\s(.*)$/i; 9 | var CodecRegexDecoders = /^\s*(.)(.)(.)(.)(.)(.)\s+(\w+)\s+(.+)\s*\(decoders:\s([^)]+)\s\)\s*(?:\(encoders:\s([^)]+)\s\))?\s*$/i; 10 | var CodecRegexEncoders = /^\s*(.)(.)(.)(.)(.)(.)\s+(\w+)\s+(.+)\s*\(encoders:\s([^)]+)\s\)\s*$/i; 11 | var CodecRegexNone = /^\s*(.)(.)(.)(.)(.)(.)\s+(\w+)\s+(.+)\s*$/i; 12 | 13 | module.exports = { 14 | 15 | ffmpeg_test: function (options) { 16 | options = options || {}; 17 | var promise = Promise.create(); 18 | var cmd = 'ffmpeg'; 19 | var args = ['-codecs']; 20 | var file = DockerPolyfill.polyfillRun({ 21 | command: options.ffmpeg_binary || "ffmpeg", 22 | argv: ["-codecs"], 23 | docker: options.docker, 24 | timeout: options.timeout 25 | }); 26 | var stderr = ""; 27 | file.stderr.on("data", function (data) { 28 | stderr += data; 29 | }); 30 | var stdout = ""; 31 | file.stdout.on("data", function (data) { 32 | stdout += data; 33 | }); 34 | var timeouted = false; 35 | file.on("timeout", function () { 36 | timeouted = true; 37 | promise.asyncError("Timeout reached"); 38 | }); 39 | file.on("close", function (status) { 40 | if (timeouted) 41 | return; 42 | if (status !== 0) { 43 | promise.asyncError("Cannot execute ffmpeg"); 44 | return; 45 | } 46 | var result = { 47 | version: {}, 48 | codecs: {}, 49 | capabilities: { 50 | auto_rotate: false 51 | }, 52 | encoders: [], 53 | decoders: [] 54 | }; 55 | stderr.split("\n").forEach(function (line) { 56 | var versionMatch = VersionRegex.exec(line); 57 | if (versionMatch) { 58 | result.version.major = parseInt(versionMatch[1], 10); 59 | result.version.minor = parseInt(versionMatch[2], 10); 60 | result.version.revision = parseInt(versionMatch[3], 10); 61 | } 62 | var configurationMatch = ConfigurationRegex.exec(line); 63 | if (configurationMatch) 64 | result.configuration = configurationMatch[1].split(" "); 65 | }); 66 | stdout.split("\n").forEach(function (line) { 67 | var decodersIdx = 9; 68 | var encodersIdx = 10; 69 | var codecMatch = CodecRegexDecoders.exec(line); 70 | if (!codecMatch) { 71 | decodersIdx = 10; 72 | encodersIdx = 9; 73 | codecMatch = CodecRegexEncoders.exec(line); 74 | if (!codecMatch) 75 | codecMatch = CodecRegexNone.exec(line); 76 | } 77 | if (codecMatch) { 78 | var codec = { 79 | support: { 80 | decoding: codecMatch[1] === 'D', 81 | encoding: codecMatch[2] === 'E', 82 | video: codecMatch[3] === 'V', 83 | audio: codecMatch[3] === 'A', 84 | intra: codecMatch[4] === 'I', 85 | lossy: codecMatch[5] === 'L', 86 | lossless: codecMatch[6] === 'S' 87 | }, 88 | short_name: codecMatch[7], 89 | long_name: codecMatch[8], 90 | decoders: codecMatch[decodersIdx] ? codecMatch[decodersIdx].split(" ") : [], 91 | encoders: codecMatch[encodersIdx] ? codecMatch[encodersIdx].split(" ") : [] 92 | }; 93 | result.codecs[codecMatch[7]] = codec; 94 | result.decoders = result.decoders.concat(codec.decoders); 95 | result.encoders = result.encoders.concat(codec.encoders); 96 | } 97 | }); 98 | if (result.version.major >= 3) 99 | result.capabilities.auto_rotate = true; 100 | promise.asyncSuccess(result); 101 | }); 102 | return promise; 103 | } 104 | 105 | }; 106 | 107 | }); 108 | 109 | -------------------------------------------------------------------------------- /src/ffmpeg-volume-detect.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise", 3 | "betajs:Types" 4 | ], function (Promise, Types) { 5 | 6 | var DockerPolyfill = require("docker-polyfill"); 7 | 8 | var mean_volume_regex = /.*mean_volume:\s*([^[\s]+)\s*/g; 9 | var max_volume_regex = /.*max_volume:\s*([^[\s]+)\s*/g; 10 | 11 | module.exports = { 12 | 13 | ffmpeg_volume_detect: function (inputFile, options) { 14 | options = options || {}; 15 | var promise = Promise.create(); 16 | var file = DockerPolyfill.polyfillRun({ 17 | command: options.ffmpeg_binary || "ffmpeg", 18 | argv: [ 19 | "-i", 20 | inputFile, 21 | "-vn", 22 | "-af", 23 | "volumedetect", 24 | "-f", 25 | "null", 26 | "/dev/null" 27 | ], 28 | docker: options.docker, 29 | timeout: options.timeout 30 | }); 31 | var lines = ""; 32 | file.stderr.on("data", function (data) { 33 | var line = data.toString(); 34 | lines += line; 35 | }); 36 | file.stderr.on("end", function (data) { 37 | lines += data; 38 | }); 39 | var timeouted = false; 40 | file.on("timeout", function () { 41 | timeouted = true; 42 | promise.asyncError("Timeout reached"); 43 | }); 44 | file.on("close", function (status) { 45 | if (timeouted) 46 | return; 47 | if (status !== 0) { 48 | promise.asyncError("Cannot read file"); 49 | return; 50 | } 51 | lines = lines.split("\n"); 52 | if (status === 0) { 53 | var mean_volume = null; 54 | var max_volume = null; 55 | lines.forEach(function (line) { 56 | var mean_volume_match = mean_volume_regex.exec(line); 57 | if (mean_volume_match) 58 | mean_volume = parseFloat(mean_volume_match[1]); 59 | var max_volume_match = max_volume_regex.exec(line); 60 | if (max_volume_match) 61 | max_volume = parseFloat(max_volume_match[1]); 62 | }); 63 | promise.asyncSuccess({ 64 | mean_volume: mean_volume, 65 | max_volume: max_volume 66 | }); 67 | } else { 68 | promise.asyncError(lines[lines.length - 2]); 69 | } 70 | }); 71 | return promise; 72 | } 73 | 74 | }; 75 | 76 | }); 77 | 78 | -------------------------------------------------------------------------------- /src/ffmpeg.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise", 3 | "betajs:Types" 4 | ], function(Promise, Types) { 5 | 6 | var DockerPolyfill = require("docker-polyfill"); 7 | var FS = require("fs"); 8 | 9 | var progress_regex = /\s*([^[=\s]+)\s*=\s*([^[=\s]+)/g; 10 | 11 | module.exports = { 12 | 13 | ffmpeg: function(files, options, output, eventCallback, eventContext, opts) { 14 | opts = opts || {}; 15 | var promise = Promise.create(); 16 | var args = []; 17 | if (Types.is_string(files)) 18 | files = [files]; 19 | files.forEach(function(file) { 20 | args.push("-i"); 21 | args.push(file); 22 | }); 23 | args = args.concat(options); 24 | if (output) { //when running the playlist script the output won't be used 25 | args.push("-y"); 26 | args.push(output); 27 | // Touch file so docker keeps the right owner 28 | FS.writeFileSync(output, ""); 29 | // console.log(args.join(" ")); 30 | } 31 | var file = DockerPolyfill.polyfillRun({ 32 | command: opts.ffmpeg_binary || "ffmpeg", 33 | argv: args.join(" ").split(" "), 34 | docker: opts.docker, 35 | timeout: opts.timeout 36 | }); 37 | var lines = ""; 38 | file.stderr.on("data", function(data) { 39 | var line = data.toString(); 40 | lines += line; 41 | if (line.indexOf("frame=") === 0) { 42 | var progress = line.trim(); 43 | var result = {}; 44 | while (true) { 45 | var m = progress_regex.exec(progress); 46 | if (!m) 47 | break; 48 | result[m[1]] = m[2]; 49 | } 50 | if (eventCallback) 51 | eventCallback.call(eventContext || this, result); 52 | } 53 | }); 54 | file.stderr.on("end", function(data) { 55 | lines += data; 56 | }); 57 | var timeouted = false; 58 | file.on("timeout", function() { 59 | timeouted = true; 60 | promise.asyncError({ 61 | message: "Timeout reached", 62 | command: args.join(" ") 63 | }); 64 | }); 65 | file.on("close", function(status) { 66 | if (timeouted) 67 | return; 68 | if (status === 0) { 69 | promise.asyncSuccess(); 70 | } else { 71 | var errlines = lines.split("\n"); 72 | promise.asyncError({ 73 | message: errlines[errlines.length - 2], 74 | logs: lines, 75 | command: args.join(" ") 76 | }); 77 | } 78 | }); 79 | return promise; 80 | } 81 | 82 | }; 83 | 84 | }); 85 | 86 | -------------------------------------------------------------------------------- /src/ffprobe-simple.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise", 3 | "betajs:Strings" 4 | ], function (Promise, Strings) { 5 | 6 | module.exports = { 7 | 8 | ffprobe_simple: function (file, options) { 9 | var parseIntUndefined = function (source, key) { 10 | return key in source ? parseInt(source[key], 10) : undefined; 11 | }; 12 | 13 | if (!Strings.STRICT_URL_REGEX.test(file) && !require('fs').existsSync(file)) 14 | return Promise.error("File does not exist"); 15 | return require(__dirname + "/ffprobe.js").ffprobe(file, options).mapSuccess(function (json) { 16 | if (!json.format || !json.streams) 17 | return Promise.error("Cannot read file"); 18 | var result = { 19 | filename: json.format.filename, 20 | stream_count: json.format.nb_streams, 21 | size: parseInt(json.format.size, 10), 22 | bit_rate: parseInt(json.format.bit_rate, 10), 23 | start_time: parseFloat(json.format.start_time), 24 | duration: parseFloat(json.format.duration), 25 | format_name: json.format.format_long_name, 26 | format_extensions: json.format.format_name.split(","), 27 | format_default_extension: Strings.splitFirst(json.format.format_name, ",").head 28 | }; 29 | json.streams.forEach(function (stream) { 30 | if (stream.codec_type === 'video') { 31 | var rotation = stream.tags && stream.tags.rotate ? parseInt(stream.tags.rotate, 10) : 0; 32 | var video = { 33 | index: stream.index, 34 | rotation: rotation, 35 | width: stream.width, 36 | height: stream.height, 37 | rotated_width: rotation % 180 === 0 ? stream.width : stream.height, 38 | rotated_height: rotation % 180 === 0 ? stream.height : stream.width, 39 | codec_name: stream.codec_tag_string, 40 | codec_long_name: stream.codec_long_name, 41 | codec_profile: stream.profile, 42 | bit_rate: parseIntUndefined(stream, "bit_rate"), 43 | frames: parseIntUndefined(stream, "nb_frames") 44 | }; 45 | if (json.format.format_name === "image" || json.format.format_name === "image2" || stream.codec_name === 'png') 46 | result.image = video; 47 | else 48 | result.video = video; 49 | } else if (stream.codec_type === 'audio') { 50 | result.audio = { 51 | index: stream.index, 52 | codec_name: stream.codec_name, 53 | codec_long_name: stream.codec_long_name, 54 | codec_profile: stream.profile, 55 | audio_channels: stream.channels, 56 | sample_rate: parseIntUndefined(stream, "sample_rate"), 57 | bit_rate: parseIntUndefined(stream, "bit_rate") 58 | }; 59 | } 60 | }); 61 | return result; 62 | }); 63 | } 64 | 65 | }; 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /src/ffprobe.js: -------------------------------------------------------------------------------- 1 | Scoped.require([ 2 | "betajs:Promise" 3 | ], function (Promise) { 4 | 5 | var DockerPolyfill = require("docker-polyfill"); 6 | 7 | module.exports = { 8 | 9 | ffprobe: function (fileName, options) { 10 | options = options || {}; 11 | var promise = Promise.create(); 12 | var file = DockerPolyfill.polyfillRun({ 13 | command: options.ffprobe_binary || "ffprobe", 14 | argv: ['-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', fileName], 15 | docker: options.docker, 16 | timeout: options.timeout 17 | }); 18 | /* 19 | var stderr = ""; 20 | file.stderr.on("data", function (data) { 21 | stderr += data; 22 | }); 23 | */ 24 | var stdout = ""; 25 | file.stdout.on("data", function (data) { 26 | stdout += data; 27 | }); 28 | var timeouted = false; 29 | file.on("timeout", function () { 30 | timeouted = true; 31 | promise.asyncError("Timeout reached"); 32 | }); 33 | file.on("close", function (status) { 34 | if (timeouted) 35 | return; 36 | if (status !== 0) { 37 | promise.asyncError("Cannot read file"); 38 | return; 39 | } 40 | try { 41 | var success = JSON.parse(stdout); 42 | promise.asyncSuccess(success); 43 | } catch (e) { 44 | promise.asyncError("FFProbe Parse error: " + stdout); 45 | } 46 | }); 47 | return promise; 48 | } 49 | 50 | }; 51 | 52 | }); 53 | 54 | -------------------------------------------------------------------------------- /tests/assets/audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jsonize/js-ffmpeg/612440a4ed055e7b06b154976124c015ee85f731/tests/assets/audio.mp3 -------------------------------------------------------------------------------- /tests/assets/etc_passwd_xbin.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jsonize/js-ffmpeg/612440a4ed055e7b06b154976124c015ee85f731/tests/assets/etc_passwd_xbin.avi -------------------------------------------------------------------------------- /tests/assets/iphone_rotated.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jsonize/js-ffmpeg/612440a4ed055e7b06b154976124c015ee85f731/tests/assets/iphone_rotated.mov -------------------------------------------------------------------------------- /tests/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jsonize/js-ffmpeg/612440a4ed055e7b06b154976124c015ee85f731/tests/assets/logo.png -------------------------------------------------------------------------------- /tests/assets/novideo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jsonize/js-ffmpeg/612440a4ed055e7b06b154976124c015ee85f731/tests/assets/novideo.mp4 -------------------------------------------------------------------------------- /tests/assets/video-640-360.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jsonize/js-ffmpeg/612440a4ed055e7b06b154976124c015ee85f731/tests/assets/video-640-360.mp4 -------------------------------------------------------------------------------- /tests/tests/ffmpeg-simple-gif.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require(__dirname + "/../../index.js"); 2 | var settings = require(__dirname + "/settings.js"); 3 | 4 | var TEMP_GIF = __dirname + "/../../temp/output-test.gif"; 5 | var VIDEO_FILE = __dirname + "/../assets/video-640-360.mp4"; 6 | 7 | QUnit.test("ffmpeg-simple gif transcoding", function(assert) { 8 | var done = assert.async(); 9 | ffmpeg.ffmpeg_simple(VIDEO_FILE, { 10 | output_type: "gif" 11 | }, TEMP_GIF, null, null, settings).callback(function (error, value) { 12 | assert.ok(!error); 13 | done(); 14 | }); 15 | }); 16 | 17 | QUnit.test("ffmpeg-simple gif with 12 fps", function(assert) { 18 | var done = assert.async(); 19 | ffmpeg.ffmpeg_simple(VIDEO_FILE, { 20 | output_type: "gif", 21 | framerate: 12 22 | }, TEMP_GIF, null, null, settings).callback(function (error, value) { 23 | assert.ok(!error); 24 | assert.equal(value.video.frames, 12); 25 | done(); 26 | }); 27 | }); 28 | 29 | QUnit.test("ffmpeg-simple gif with 320 width", function(assert) { 30 | var done = assert.async(); 31 | ffmpeg.ffmpeg_simple(VIDEO_FILE, { 32 | output_type: "gif", 33 | width: 320 34 | }, TEMP_GIF, null, null, settings).callback(function (error, value) { 35 | assert.ok(!error); 36 | assert.equal(value.video.width, 320); 37 | done(); 38 | }); 39 | }); 40 | 41 | QUnit.test("ffmpeg-simple gif with 240 height", function(assert) { 42 | var done = assert.async(); 43 | ffmpeg.ffmpeg_simple(VIDEO_FILE, { 44 | output_type: "gif", 45 | height: 240 46 | }, TEMP_GIF, null, null, settings).callback(function (error, value) { 47 | assert.ok(!error); 48 | assert.equal(value.video.height, 240); 49 | done(); 50 | }); 51 | }); 52 | 53 | QUnit.test("ffmpeg-simple gif with 0.8 second duration", function(assert) { 54 | var done = assert.async(); 55 | ffmpeg.ffmpeg_simple(VIDEO_FILE, { 56 | output_type: "gif", 57 | time_end: 0.8 58 | }, TEMP_GIF, null, null, settings).callback(function (error, value) { 59 | assert.ok(!error); 60 | assert.equal(value.duration, 0.8); 61 | done(); 62 | }); 63 | }); 64 | 65 | -------------------------------------------------------------------------------- /tests/tests/ffmpeg-simple-image.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require(__dirname + "/../../index.js"); 2 | var settings = require(__dirname + "/settings.js"); 3 | 4 | var TEMP_IMAGE = __dirname + "/../../temp/output-test-watermark.png"; 5 | var WATERMARK_FILE = __dirname + "/../assets/logo.png"; 6 | var EXPLOIT_FILE = __dirname + "/../assets/etx_passwd_xbin.avi"; 7 | 8 | QUnit.test("ffmpeg-simple with logo", function(assert) { 9 | var done = assert.async(); 10 | ffmpeg.ffmpeg_simple(WATERMARK_FILE, { 11 | output_type: "image" 12 | }, TEMP_IMAGE, null, null, settings).callback(function (error, value) { 13 | assert.ok(!error); 14 | done(); 15 | }); 16 | }); 17 | 18 | 19 | QUnit.test("ffmpeg-simple exploit", function(assert) { 20 | var done = assert.async(); 21 | ffmpeg.ffmpeg_simple(EXPLOIT_FILE, { 22 | output_type: "image" 23 | }, TEMP_IMAGE, null, null, settings).callback(function (error, value) { 24 | assert.ok(error); 25 | done(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/tests/ffmpeg-simple-rotation.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require(__dirname + "/../../index.js"); 2 | var settings = require(__dirname + "/settings.js"); 3 | 4 | var ROTATED_MOV_VIDEO = __dirname + "/../assets/iphone_rotated.mov"; 5 | var TEMP_MP4_VIDEO = __dirname + "/../../temp/output-test.mp4"; 6 | var STANDARD_MP4 = __dirname + "/../assets/video-640-360.mp4"; 7 | 8 | 9 | QUnit.test("ffmpeg-simple mov to mp4", function(assert) { 10 | var done = assert.async(); 11 | ffmpeg.ffmpeg_simple(ROTATED_MOV_VIDEO, {}, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 12 | assert.ok(!error); 13 | assert.equal(value.video.rotation, 0); 14 | assert.equal(value.video.width, 320); 15 | assert.equal(value.video.height, 568); 16 | done(); 17 | }); 18 | }); 19 | 20 | QUnit.test("ffmpeg-simple mov to mp4 2", function(assert) { 21 | var done = assert.async(); 22 | ffmpeg.ffmpeg_simple(ROTATED_MOV_VIDEO, {rotate: 90}, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 23 | assert.ok(!error); 24 | assert.equal(value.video.rotation, 0); 25 | assert.equal(value.video.width, 568); 26 | assert.equal(value.video.height, 320); 27 | done(); 28 | }); 29 | }); 30 | 31 | QUnit.test("ffmpeg-simple mp4 to mp4", function(assert) { 32 | var done = assert.async(); 33 | ffmpeg.ffmpeg_simple(STANDARD_MP4, {}, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 34 | assert.ok(!error); 35 | assert.equal(value.video.rotation, 0); 36 | assert.equal(value.video.width, 640); 37 | assert.equal(value.video.height, 360); 38 | done(); 39 | }); 40 | }); 41 | 42 | QUnit.test("ffmpeg-simple mp4 to mp4 2", function(assert) { 43 | var done = assert.async(); 44 | ffmpeg.ffmpeg_simple(STANDARD_MP4, {rotate: 90}, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 45 | assert.ok(!error); 46 | assert.equal(value.video.rotation, 0); 47 | assert.equal(value.video.width, 360); 48 | assert.equal(value.video.height, 640); 49 | done(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/tests/ffmpeg-simple-sizing.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require(__dirname + "/../../index.js"); 2 | var settings = require(__dirname + "/settings.js"); 3 | 4 | var STANDARD_MP4 = __dirname + "/../assets/video-640-360.mp4"; 5 | var TEMP_MP4_VIDEO = __dirname + "/../../temp/output-test.mp4"; 6 | 7 | QUnit.test("ffmpeg-simple shrink same fixed ratio", function(assert) { 8 | var done = assert.async(); 9 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 10 | width: 320, 11 | height: 180, 12 | ratio_strategy: "fixed" 13 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 14 | assert.ok(!error); 15 | assert.equal(value.video.width, 320); 16 | assert.equal(value.video.height, 180); 17 | done(); 18 | }); 19 | }); 20 | 21 | QUnit.test("ffmpeg-simple stretch same fixed ratio", function(assert) { 22 | var done = assert.async(); 23 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 24 | width: 960, 25 | height: 540, 26 | ratio_strategy: "fixed", 27 | stretch_strategy: "stretch-crop" 28 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 29 | assert.ok(!error); 30 | assert.equal(value.video.width, 960); 31 | assert.equal(value.video.height, 540); 32 | done(); 33 | }); 34 | }); 35 | 36 | QUnit.test("ffmpeg-simple shrink smaller ratio, shrink ratio", function(assert) { 37 | var done = assert.async(); 38 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 39 | width: 320, 40 | height: 200, 41 | ratio_strategy: "shrink" 42 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 43 | assert.ok(!error); 44 | assert.equal(value.video.width, 320); 45 | assert.equal(value.video.height, 180); 46 | done(); 47 | }); 48 | }); 49 | 50 | QUnit.test("ffmpeg-simple shrink greater ratio, shrink ratio", function(assert) { 51 | var done = assert.async(); 52 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 53 | width: 400, 54 | height: 180, 55 | ratio_strategy: "shrink" 56 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 57 | assert.ok(!error); 58 | assert.equal(value.video.width, 320); 59 | assert.equal(value.video.height, 180); 60 | done(); 61 | }); 62 | }); 63 | 64 | 65 | 66 | QUnit.test("ffmpeg-simple shrink smaller ratio, stretch ratio", function(assert) { 67 | var done = assert.async(); 68 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 69 | width: 300, 70 | height: 180, 71 | ratio_strategy: "stretch" 72 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 73 | assert.ok(!error); 74 | assert.equal(value.video.width, 320); 75 | assert.equal(value.video.height, 180); 76 | done(); 77 | }); 78 | }); 79 | 80 | QUnit.test("ffmpeg-simple shrink greater ratio, stretch ratio", function(assert) { 81 | var done = assert.async(); 82 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 83 | width: 320, 84 | height: 140, 85 | ratio_strategy: "stretch" 86 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 87 | assert.ok(!error); 88 | assert.equal(value.video.width, 320); 89 | assert.equal(value.video.height, 180); 90 | done(); 91 | }); 92 | }); 93 | 94 | QUnit.test("ffmpeg-simple shrink smaller ratio, fixed ratio, shrink-pad", function (assert) { 95 | var done = assert.async(); 96 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 97 | width: 240, 98 | height: 180, 99 | ratio_strategy: "fixed", 100 | shrink_strategy: "shrink-pad" 101 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 102 | assert.ok(!error); 103 | assert.equal(value.video.width, 240); 104 | assert.equal(value.video.height, 180); 105 | done(); 106 | }); 107 | }); 108 | 109 | QUnit.test("ffmpeg-simple shrink greater ratio, fixed ratio, shrink-pad", function (assert) { 110 | var done = assert.async(); 111 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 112 | width: 360, 113 | height: 140, 114 | ratio_strategy: "fixed", 115 | shrink_strategy: "shrink-pad" 116 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 117 | assert.ok(!error); 118 | assert.equal(value.video.width, 360); 119 | assert.equal(value.video.height, 140); 120 | done(); 121 | }); 122 | }); 123 | 124 | QUnit.test("ffmpeg-simple shrink smaller ratio, fixed ratio, shrink-crop", function (assert) { 125 | var done = assert.async(); 126 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 127 | width: 240, 128 | height: 180, 129 | ratio_strategy: "fixed", 130 | shrink_strategy: "shrink-crop" 131 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 132 | assert.ok(!error); 133 | assert.equal(value.video.width, 240); 134 | assert.equal(value.video.height, 180); 135 | done(); 136 | }); 137 | }); 138 | 139 | QUnit.test("ffmpeg-simple shrink greater ratio, fixed ratio, shrink-crop", function (assert) { 140 | var done = assert.async(); 141 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 142 | width: 360, 143 | height: 140, 144 | ratio_strategy: "fixed", 145 | shrink_strategy: "shrink-crop" 146 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 147 | assert.ok(!error); 148 | assert.equal(value.video.width, 360); 149 | assert.equal(value.video.height, 140); 150 | done(); 151 | }); 152 | }); 153 | 154 | QUnit.test("ffmpeg-simple shrink smaller ratio, fixed ratio, crop", function (assert) { 155 | var done = assert.async(); 156 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 157 | width: 240, 158 | height: 180, 159 | ratio_strategy: "fixed", 160 | shrink_strategy: "crop" 161 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 162 | assert.ok(!error); 163 | assert.equal(value.video.width, 240); 164 | assert.equal(value.video.height, 180); 165 | done(); 166 | }); 167 | }); 168 | 169 | QUnit.test("ffmpeg-simple shrink greater ratio, fixed ratio, crop", function (assert) { 170 | var done = assert.async(); 171 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 172 | width: 360, 173 | height: 140, 174 | ratio_strategy: "fixed", 175 | shrink_strategy: "crop" 176 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 177 | assert.ok(!error); 178 | assert.equal(value.video.width, 360); 179 | assert.equal(value.video.height, 140); 180 | done(); 181 | }); 182 | }); 183 | 184 | 185 | 186 | QUnit.test("ffmpeg-simple stretch smaller ratio, fixed ratio, stretch-pad", function (assert) { 187 | var done = assert.async(); 188 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 189 | width: 960, 190 | height: 600, 191 | ratio_strategy: "fixed", 192 | stretch_strategy: "stretch-pad" 193 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 194 | assert.ok(!error); 195 | assert.equal(value.video.width, 960); 196 | assert.equal(value.video.height, 600); 197 | done(); 198 | }); 199 | }); 200 | 201 | 202 | 203 | QUnit.test("ffmpeg-simple stretch greater ratio, fixed ratio, stretch-pad", function (assert) { 204 | var done = assert.async(); 205 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 206 | width: 960, 207 | height: 500, 208 | ratio_strategy: "fixed", 209 | stretch_strategy: "stretch-pad" 210 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 211 | assert.ok(!error); 212 | assert.equal(value.video.width, 960); 213 | assert.equal(value.video.height, 500); 214 | done(); 215 | }); 216 | }); 217 | 218 | QUnit.test("ffmpeg-simple stretch smaller ratio, fixed ratio, stretch-crop", function (assert) { 219 | var done = assert.async(); 220 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 221 | width: 960, 222 | height: 600, 223 | ratio_strategy: "fixed", 224 | stretch_strategy: "stretch-crop" 225 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 226 | assert.ok(!error); 227 | assert.equal(value.video.width, 960); 228 | assert.equal(value.video.height, 600); 229 | done(); 230 | }); 231 | }); 232 | 233 | QUnit.test("ffmpeg-simple stretch greater ratio, fixed ratio, stretch-crop", function (assert) { 234 | var done = assert.async(); 235 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 236 | width: 960, 237 | height: 500, 238 | ratio_strategy: "fixed", 239 | stretch_strategy: "stretch-crop" 240 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 241 | assert.ok(!error); 242 | assert.equal(value.video.width, 960); 243 | assert.equal(value.video.height, 500); 244 | done(); 245 | }); 246 | }); 247 | 248 | 249 | QUnit.test("ffmpeg-simple stretch smaller ratio, fixed ratio, pad", function (assert) { 250 | var done = assert.async(); 251 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 252 | width: 960, 253 | height: 600, 254 | ratio_strategy: "fixed", 255 | stretch_strategy: "pad" 256 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 257 | assert.ok(!error); 258 | assert.equal(value.video.width, 960); 259 | assert.equal(value.video.height, 600); 260 | done(); 261 | }); 262 | }); 263 | 264 | QUnit.test("ffmpeg-simple stretch greater ratio, fixed ratio, pad", function (assert) { 265 | var done = assert.async(); 266 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 267 | width: 960, 268 | height: 500, 269 | ratio_strategy: "fixed", 270 | stretch_strategy: "pad" 271 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 272 | assert.ok(!error); 273 | assert.equal(value.video.width, 960); 274 | assert.equal(value.video.height, 500); 275 | done(); 276 | }); 277 | }); 278 | 279 | QUnit.test("ffmpeg-simple mixed smaller ratio, fixed ratio, stretch-crop", function (assert) { 280 | var done = assert.async(); 281 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 282 | width: 600, 283 | height: 400, 284 | ratio_strategy: "fixed", 285 | mixed_strategy: "stretch-crop" 286 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 287 | assert.ok(!error); 288 | assert.equal(value.video.width, 600); 289 | assert.equal(value.video.height, 400); 290 | done(); 291 | }); 292 | }); 293 | 294 | 295 | QUnit.test("ffmpeg-simple mixed greater ratio, fixed ratio, stretch-crop", function (assert) { 296 | var done = assert.async(); 297 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 298 | width: 700, 299 | height: 300, 300 | ratio_strategy: "fixed", 301 | mixed_strategy: "stretch-crop" 302 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 303 | assert.ok(!error); 304 | assert.equal(value.video.width, 700); 305 | assert.equal(value.video.height, 300); 306 | done(); 307 | }); 308 | }); 309 | 310 | QUnit.test("ffmpeg-simple mixed smaller ratio, fixed ratio, shrink-pad", function (assert) { 311 | var done = assert.async(); 312 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 313 | width: 600, 314 | height: 400, 315 | ratio_strategy: "fixed", 316 | mixed_strategy: "shrink-pad" 317 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 318 | assert.ok(!error); 319 | assert.equal(value.video.width, 600); 320 | assert.equal(value.video.height, 400); 321 | done(); 322 | }); 323 | }); 324 | 325 | QUnit.test("ffmpeg-simple mixed greater ratio, fixed ratio, shrink-crop", function (assert) { 326 | var done = assert.async(); 327 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 328 | width: 700, 329 | height: 300, 330 | ratio_strategy: "fixed", 331 | mixed_strategy: "shrink-pad" 332 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 333 | assert.ok(!error); 334 | assert.equal(value.video.width, 700); 335 | assert.equal(value.video.height, 300); 336 | done(); 337 | }); 338 | }); 339 | 340 | 341 | QUnit.test("ffmpeg-simple mixed smaller ratio, fixed ratio, crop-pad", function (assert) { 342 | var done = assert.async(); 343 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 344 | width: 600, 345 | height: 400, 346 | ratio_strategy: "fixed", 347 | mixed_strategy: "crop-pad" 348 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 349 | assert.ok(!error); 350 | assert.equal(value.video.width, 600); 351 | assert.equal(value.video.height, 400); 352 | done(); 353 | }); 354 | }); 355 | 356 | QUnit.test("ffmpeg-simple mixed greater ratio, fixed ratio, crop-pad", function (assert) { 357 | var done = assert.async(); 358 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 359 | width: 700, 360 | height: 300, 361 | ratio_strategy: "fixed", 362 | mixed_strategy: "crop-pad" 363 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 364 | assert.ok(!error); 365 | assert.equal(value.video.width, 700); 366 | assert.equal(value.video.height, 300); 367 | done(); 368 | }); 369 | }); 370 | 371 | 372 | -------------------------------------------------------------------------------- /tests/tests/ffmpeg-simple-watermarks.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require(__dirname + "/../../index.js"); 2 | var settings = require(__dirname + "/settings.js"); 3 | 4 | var STANDARD_MP4 = __dirname + "/../assets/video-640-360.mp4"; 5 | var TEMP_MP4_VIDEO = __dirname + "/../../temp/output-test-watermark.mp4"; 6 | var WATERMARK_FILE = __dirname + "/../assets/logo.png"; 7 | 8 | QUnit.test("ffmpeg-simple with logo", function(assert) { 9 | var done = assert.async(); 10 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 11 | watermark: WATERMARK_FILE 12 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 13 | assert.ok(!error); 14 | done(); 15 | }); 16 | }); 17 | 18 | 19 | 20 | QUnit.test("ffmpeg-simple with double logo", function(assert) { 21 | var done = assert.async(); 22 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 23 | watermarks: [{ 24 | watermark: WATERMARK_FILE, 25 | watermark_size: 0.25, 26 | watermark_x: 0.95, 27 | watermark_y: 0.95 28 | }, { 29 | watermark: WATERMARK_FILE, 30 | watermark_size: 0.25, 31 | watermark_x: 0.05, 32 | watermark_y: 0.05 33 | }] 34 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 35 | assert.ok(!error); 36 | done(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/tests/ffmpeg-volume-detect.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require(__dirname + "/../../index.js"); 2 | var settings = require(__dirname + "/settings.js"); 3 | 4 | var STANDARD_MP4 = __dirname + "/../assets/video-640-360.mp4"; 5 | var TEMP_MP4_VIDEO = __dirname + "/../../temp/output-test-volume.mp4"; 6 | 7 | QUnit.test("ffmpeg volume detect", function(assert) { 8 | var done = assert.async(); 9 | ffmpeg.ffmpeg_volume_detect(STANDARD_MP4, settings).callback(function (error, value) { 10 | assert.equal(value.mean_volume, -36.5); 11 | assert.equal(value.max_volume, -25.8); 12 | assert.ok(!error); 13 | done(); 14 | }); 15 | }); 16 | 17 | 18 | QUnit.test("ffmpeg volume normalization", function(assert) { 19 | var done = assert.async(); 20 | ffmpeg.ffmpeg_simple(STANDARD_MP4, { 21 | normalize_audio: true 22 | }, TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 23 | assert.ok(!error); 24 | done(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/tests/ffmpeg.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require(__dirname + "/../../index.js"); 2 | var settings = require(__dirname + "/settings.js"); 3 | 4 | var ROTATED_MOV_VIDEO = __dirname + "/../assets/iphone_rotated.mov"; 5 | var TEMP_MP4_VIDEO = __dirname + "/../../temp/output-test.mp4"; 6 | var EXPLOIT_FILE = __dirname + "/../assets/etx_passwd_xbin.avi"; 7 | 8 | 9 | QUnit.test("ffmpeg mov to mp4", function(assert) { 10 | var done = assert.async(); 11 | ffmpeg.ffmpeg(ROTATED_MOV_VIDEO, [], TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 12 | assert.ok(!error); 13 | done(); 14 | }); 15 | }); 16 | 17 | QUnit.test("ffmpeg timeout", function (assert) { 18 | var done = assert.async(); 19 | ffmpeg.ffmpeg(ROTATED_MOV_VIDEO, [], TEMP_MP4_VIDEO, null, null, BetaJS.Objs.extend({timeout: 1}, settings)).callback(function (error, value) { 20 | assert.equal(error.message, "Timeout reached"); 21 | done(); 22 | }); 23 | }); 24 | 25 | QUnit.test("ffmpeg exploit", function (assert) { 26 | var done = assert.async(); 27 | ffmpeg.ffmpeg(EXPLOIT_FILE, [], TEMP_MP4_VIDEO, null, null, settings).callback(function (error, value) { 28 | assert.ok(error); 29 | done(); 30 | }); 31 | }); -------------------------------------------------------------------------------- /tests/tests/ffprobe-simple.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require(__dirname + "/../../index.js"); 2 | var settings = require(__dirname + "/settings.js"); 3 | 4 | var NOT_EXISTING_VIDEO = __dirname + "/notexisting.mp4"; 5 | var ROTATED_MOV_VIDEO = __dirname + "/../assets/iphone_rotated.mov"; 6 | var NO_VIDEO = __dirname + "/../assets/novideo.mp4"; 7 | var EXPLOIT_FILE = __dirname + "/../assets/etx_passwd_xbin.avi"; 8 | 9 | QUnit.test("ffprobe-simple not existing", function(assert) { 10 | var done = assert.async(); 11 | ffmpeg.ffprobe_simple(NOT_EXISTING_VIDEO, settings).callback(function(error, value) { 12 | assert.equal(error, 'File does not exist'); 13 | done(); 14 | }); 15 | }); 16 | 17 | QUnit.test("ffprobe-simple no video", function(assert) { 18 | var done = assert.async(); 19 | ffmpeg.ffprobe_simple(NO_VIDEO, settings).callback(function(error, value) { 20 | assert.equal(error, 'Cannot read file'); 21 | done(); 22 | }); 23 | }); 24 | 25 | QUnit.test("ffprobe-simple rotated mov", function(assert) { 26 | var done = assert.async(); 27 | ffmpeg.ffprobe_simple(ROTATED_MOV_VIDEO, settings).callback(function(error, value) { 28 | delete value.filename; 29 | delete value.format_name; 30 | delete value.audio.codec_long_name; 31 | delete value.audio.codec_profile; 32 | delete value.video.codec_long_name; 33 | delete value.video.codec_profile; 34 | assert.deepEqual(value, { 35 | //filename : ROTATED_MOV_VIDEO, 36 | stream_count : 2, 37 | size : 159993, 38 | bit_rate : 581352, 39 | start_time : 0, 40 | duration : 2.201667, 41 | //format_name : 'QuickTime / MOV', 42 | format_extensions : [ 'mov', 'mp4', 'm4a', '3gp', '3g2', 'mj2' ], 43 | format_default_extension : 'mov', 44 | audio : { 45 | index : 0, 46 | codec_name : 'aac', 47 | //codec_long_name : 'AAC (Advanced Audio Coding)', 48 | //codec_profile : 'LC', 49 | audio_channels : 1, 50 | sample_rate : 44100, 51 | bit_rate : 61893 52 | }, 53 | video : { 54 | index : 1, 55 | rotation : 90, 56 | width : 568, 57 | height : 320, 58 | rotated_width : 320, 59 | rotated_height : 568, 60 | codec_name : 'avc1', 61 | //codec_long_name : 'H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10', 62 | //codec_profile : 'Baseline', 63 | bit_rate : 507677, 64 | frames: 66 65 | } 66 | }); 67 | done(); 68 | }); 69 | }); 70 | 71 | QUnit.test("ffprobe-simple exploit", function (assert) { 72 | var done = assert.async(); 73 | ffmpeg.ffprobe_simple(EXPLOIT_FILE, settings).callback(function (error, value) { 74 | assert.ok(error); 75 | done(); 76 | }); 77 | }); -------------------------------------------------------------------------------- /tests/tests/ffprobe.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require(__dirname + "/../../index.js"); 2 | var settings = require(__dirname + "/settings.js"); 3 | 4 | var ROTATED_MOV_VIDEO = __dirname + "/../assets/iphone_rotated.mov"; 5 | var IMAGE_FILE = __dirname + "/../assets/logo.png"; 6 | 7 | var EXPLOIT_FILE = __dirname + "/../assets/etx_passwd_xbin.avi"; 8 | 9 | QUnit.test("ffprobe rotated mov", function (assert) { 10 | var done = assert.async(); 11 | ffmpeg.ffprobe(ROTATED_MOV_VIDEO, settings).callback(function (error, value) { 12 | assert.deepEqual(value.format.nb_streams, 2); 13 | done(); 14 | }); 15 | }); 16 | 17 | QUnit.test("ffprobe image", function (assert) { 18 | var done = assert.async(); 19 | ffmpeg.ffprobe(IMAGE_FILE, settings).callback(function(error, value) { 20 | assert.deepEqual(value.format.nb_streams, 1); 21 | done(); 22 | }); 23 | }); 24 | 25 | 26 | QUnit.test("ffprobe exploit", function (assert) { 27 | var done = assert.async(); 28 | ffmpeg.ffprobe(EXPLOIT_FILE, settings).callback(function(error, value) { 29 | assert.ok(error); 30 | done(); 31 | }); 32 | }); -------------------------------------------------------------------------------- /tests/tests/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_ffmpeg: true 3 | /* 4 | test_info: { 5 | capabilities: { 6 | auto_rotate: true 7 | } 8 | }, 9 | */ 10 | /* 11 | docker: { 12 | "container": "jrottenberg/ffmpeg", 13 | "proxy": "localhost:1234", 14 | "replaceArguments": { 15 | "libfaac": "libfdk_aac", 16 | "^/var": "/private/var" 17 | }, 18 | "preprocessFiles": { 19 | "chown": "TODO", 20 | "chmod": 666, 21 | "mkdirs": true 22 | }, 23 | "postprocessFiles": { 24 | "chown": "daemon", 25 | "chmod": 666, 26 | "recoverChown": true, 27 | "recoverChmod": true 28 | } 29 | }*/ 30 | }; -------------------------------------------------------------------------------- /types/betajs.d.ts: -------------------------------------------------------------------------------- 1 | export type BetaJSPromise = { 2 | toNativePromise: () => Promise 3 | } 4 | -------------------------------------------------------------------------------- /types/ffmpeg-faststart.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFmpegOpts } from "./opts"; 3 | import { TFFmpegProgress } from "./ffmpeg"; 4 | 5 | export function ffmpeg_faststart( 6 | file: string | string[], 7 | output?: string, 8 | eventCallback?: (progress: TFFmpegProgress) => unknown, 9 | eventContext?: Record, 10 | opts?: TFFmpegOpts 11 | ): BetaJSPromise; 12 | -------------------------------------------------------------------------------- /types/ffmpeg-graceful.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFmpegSimpleOptions, TFFmpegSimpleProgress, TFFmpegSimpleResponse } from "./ffmpeg-simple"; 3 | import { TOpts } from "./opts"; 4 | 5 | export function ffmpeg_graceful( 6 | files: string | string[], 7 | options?: TFFmpegSimpleOptions, 8 | output?: string, 9 | eventCallback?: (progress: TFFmpegSimpleProgress) => unknown, 10 | eventContext?: Record, 11 | opts?: TOpts 12 | ): BetaJSPromise; 13 | -------------------------------------------------------------------------------- /types/ffmpeg-multi-pass.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFmpegProgress } from "./ffmpeg"; 3 | 4 | export function ffmpeg_multi_pass( 5 | files: string | string[], 6 | options?: string[], 7 | passes?: number, 8 | output?: string, 9 | eventCallback?: (progress: TFFmpegProgress) => unknown, 10 | eventContext?: Record 11 | ): BetaJSPromise; 12 | -------------------------------------------------------------------------------- /types/ffmpeg-playlist.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFmpegSimpleOptions, TFFmpegSimpleProgress } from "./ffmpeg-simple" 3 | import { TOpts } from "./opts"; 4 | 5 | export function ffmpeg_playlist( 6 | files: string | string[], 7 | options?: TFFmpegPlaylistOptions, 8 | output?: string, 9 | eventCallback?: (progress: TFFmpegSimpleProgress) => unknown, 10 | eventContext?: Record, 11 | opts?: TOpts 12 | ): BetaJSPromise; 13 | 14 | export function ffmpeg_playlist_raw( 15 | files: string | string[], 16 | options?: TFFmpegPlaylistOptions, 17 | output?: string, 18 | eventCallback?: (progress: TFFmpegSimpleProgress) => unknown, 19 | eventContext?: Record, 20 | opts?: TOpts 21 | ): BetaJSPromise; 22 | 23 | export type TFFmpegPlaylistReponse = {playlist: `${string}/playlist.m3u8`}; 24 | 25 | export type TFFmpegPlaylistOptions = TFFmpegSimpleOptions & { 26 | segment_target_duration?: number, 27 | max_bitrate_ratio?: number, 28 | rate_monitor_buffer_ratio?: number, 29 | key_frames_interval?: number, 30 | renditions: TRendition[] 31 | }; 32 | 33 | export type TRendition = { 34 | resolution: `${number}x${number}`, 35 | bitrate: number, 36 | audio_rate: number 37 | }; 38 | -------------------------------------------------------------------------------- /types/ffmpeg-simple.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFProbeSimpleResponse } from "./ffprobe-simple"; 3 | import { TOpts } from "./opts"; 4 | 5 | export function ffmpeg_simple_raw( 6 | files: string | string[], 7 | options?: TFFmpegSimpleOptions, 8 | output?: string, 9 | eventCallback?: (progress: TFFmpegSimpleProgress) => unknown, 10 | eventContext?: Record, 11 | opts?: TOpts 12 | ): BetaJSPromise; 13 | 14 | export function ffmpeg_simple( 15 | files: string | string[], 16 | options?: TFFmpegSimpleOptions, 17 | output?: string, 18 | eventCallback?: (progress: TFFmpegSimpleProgress) => unknown, 19 | eventContext?: Record, 20 | opts?: TOpts 21 | ): BetaJSPromise; 22 | 23 | export type TFFmpegSimpleResponse = TFFProbeSimpleResponse; 24 | 25 | export type TFFmpegSimpleProgress = { 26 | frame?: number, 27 | fps?: number, 28 | q?: number, 29 | size_kb?: number, 30 | bitrate_kbits?: number, 31 | dup?: number, 32 | drop?: number, 33 | time?: number, 34 | pass: number, 35 | passes: number, 36 | progress?: number 37 | }; 38 | 39 | export type TFFmpegSimpleOptions = { 40 | output_type?: "video" | "audio" | "image" | "gif", 41 | synchronize?: boolean, 42 | framerate?: number, 43 | framerate_gop?: number, 44 | image_percentage?: number, 45 | image_position?: number, 46 | time_limit?: number, 47 | time_start?: number, 48 | time_end?: number, 49 | video_map?: number, 50 | audio_map?: number, 51 | video_profile?: string, 52 | faststart?: boolean, 53 | video_format?: string, 54 | audio_bit_rate?: number, 55 | video_bit_rate?: number, 56 | normalize_audio?: boolean, 57 | remove_audio?: boolean, 58 | width?: number, 59 | height?: number, 60 | auto_rotate?: boolean, 61 | rotate?: number, 62 | ratio_strategy?: "fixed" | "shrink" | "stretch", 63 | size_strategy: "keep" | "shrink" | "stretch", 64 | shrink_strategy?: "shrink-pad" | "crop" | "shrink-crop", 65 | stretch_strategy?: "pad" | "stretch-pad" | "stretch-crop", 66 | mixed_strategy?: "shrink-pad" | "stretch-crop" | "crop-pad", 67 | watermarks?: TWatermark[], 68 | maxMuxingQueueSize?: boolean, 69 | } & Partial; 70 | 71 | export type TWatermark = { 72 | watermark: string, 73 | watermark_size: number, 74 | watermark_x: number, 75 | watermark_y: number, 76 | }; 77 | -------------------------------------------------------------------------------- /types/ffmpeg-test.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFmpegOpts } from "./opts"; 3 | 4 | export function ffmpeg_test( 5 | options: TFFmpegOpts 6 | ): BetaJSPromise; 7 | 8 | export type TFFmpegTestResponse = { 9 | version: TVersion, 10 | configuration?: string[], 11 | codecs: TCodec, 12 | capabilities: TCapabilities, 13 | encoders: string[], 14 | decoders: string[] 15 | }; 16 | 17 | type TVersion = { 18 | major: number, 19 | minor: number, 20 | revision: number 21 | } | Record; 22 | 23 | type TCapabilities = { 24 | auto_rotate: boolean 25 | }; 26 | 27 | type TCodec = { 28 | support: { 29 | decoding: boolean, 30 | encoding: boolean, 31 | video: boolean, 32 | audio: boolean, 33 | intra: boolean, 34 | lossy: boolean, 35 | lossless: boolean 36 | }, 37 | short_name: string, 38 | long_name: string, 39 | decoders: string[], 40 | encoders: string[] 41 | } | Record; 42 | -------------------------------------------------------------------------------- /types/ffmpeg-volume-detect.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFmpegOpts } from "./opts"; 3 | 4 | export function ffmpeg_volume_detect( 5 | file: string, 6 | options?: TFFmpegOpts 7 | ): BetaJSPromise; 8 | 9 | export type TFFmpegVolumeDetectResponse = { 10 | mean_volume: number, 11 | max_volume: number 12 | }; 13 | -------------------------------------------------------------------------------- /types/ffmpeg.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFmpegOpts } from "./opts"; 3 | 4 | export function ffmpeg( 5 | files: string | string[], 6 | options?: string[], 7 | output?: string, 8 | eventCallback?: (progress: TFFmpegProgress) => unknown, 9 | eventContext?: Record, 10 | opts?: TFFmpegOpts 11 | ): BetaJSPromise; 12 | 13 | export type TFFmpegProgress = { 14 | // TODO 15 | }; 16 | -------------------------------------------------------------------------------- /types/ffprobe-simple.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFProbeOpts } from "./opts"; 3 | 4 | export function ffprobe_simple( 5 | file: string, 6 | options?: TFFProbeOpts 7 | ): BetaJSPromise; 8 | 9 | export type TFFProbeSimpleResponse = { 10 | filename: string, 11 | stream_count: number, 12 | size: number, 13 | bit_rate: number, 14 | start_time: number, 15 | duration: number, 16 | format_name: string, 17 | format_extensions: string[], 18 | format_default_extension: string, 19 | audio?: TFFProbeSimpleAudio, 20 | image?: TFFProbeSimpleImage, 21 | video?: TFFProbeSimpleVideo 22 | }; 23 | 24 | export type TFFProbeSimpleMedia = { 25 | index: number, 26 | codec_name: string, 27 | codec_long_name: string, 28 | codec_profile: string, 29 | bit_rate?: number 30 | } 31 | 32 | export type TFFProbeSimpleAudio = TFFProbeSimpleMedia & { 33 | audio_channels: number, 34 | sample_rate?: number, 35 | }; 36 | 37 | export type TFFProbeSimpleImage = TFFProbeSimpleMedia & { 38 | rotation: number, 39 | width: number, 40 | height: number, 41 | rotated_width: number, 42 | rotated_height: number, 43 | frames?: number 44 | }; 45 | 46 | export type TFFProbeSimpleVideo = TFFProbeSimpleImage; 47 | -------------------------------------------------------------------------------- /types/ffprobe.d.ts: -------------------------------------------------------------------------------- 1 | import { BetaJSPromise } from "./betajs"; 2 | import { TFFProbeOpts } from "./opts"; 3 | 4 | export function ffprobe( 5 | file: string, 6 | options?: TFFProbeOpts 7 | ): BetaJSPromise; 8 | 9 | export type TFFProbeResponse = { 10 | streams: TFFProbeStream[], 11 | format: TFFProbeFormat 12 | }; 13 | 14 | export type TFFProbeFormat = TFFProbeAudioFormat | TFFProbeImageFormat | TFFProbeVideoFormat; 15 | 16 | type TBaseFormat = { 17 | filename: string, 18 | nb_streams: number, 19 | nb_programs: number, 20 | format_name: string, 21 | format_long_name: string, 22 | size: string, 23 | probe_score: number, 24 | tags: TTags 25 | }; 26 | 27 | export type TFFProbeAudioFormat = TBaseFormat & { 28 | start_time: string, 29 | duration: string, 30 | bit_rate: string, 31 | }; 32 | 33 | export type TFFProbeImageFormat = TBaseFormat & { 34 | start_time?: string, 35 | duration?: string, 36 | bit_rate?: string 37 | }; 38 | 39 | export type TFFProbeVideoFormat = TBaseFormat & { 40 | start_time: string, 41 | duration: string, 42 | bit_rate: string 43 | } 44 | 45 | export type TFFProbeStream = TFFProbeAudioStream | TFFProbeImageStream | TFFProbeVideoStream; 46 | 47 | type TBaseStream = { 48 | index: number, 49 | codec_name: string, 50 | codec_long_name: string, 51 | profile?: string, 52 | codec_type: string, 53 | codec_tag_string: string, 54 | codec_tag: string, 55 | id?: string, 56 | tags: TTags, 57 | disposition: TDisposition 58 | }; 59 | 60 | export type TFFProbeAudioStream = TBaseStream & { 61 | sample_fmt: string, 62 | sample_rate: string, 63 | channels: number, 64 | channel_layout: string, 65 | bits_per_sample: number, 66 | r_frame_rate: string, 67 | avg_frame_rate: string, 68 | time_base: string, 69 | start_pts: number, 70 | start_time: string, 71 | duration_ts: number, 72 | duration: string, 73 | bit_rate: string, 74 | nb_frames?: string, 75 | extradata_size?: number 76 | } 77 | 78 | export type TFFProbeImageStream = TBaseStream & { 79 | width: number, 80 | height: number, 81 | coded_width: number, 82 | coded_height: number, 83 | closed_captions: number, 84 | film_grain: number, 85 | has_b_frames: number, 86 | sample_aspect_ratio?: string, 87 | display_aspect_ratio?: string, 88 | pix_fmt: string, 89 | level: number, 90 | color_range: string, 91 | color_space?: string, 92 | chroma_location?: string, 93 | refs: number, 94 | r_frame_rate: string, 95 | avg_frame_rate: string, 96 | time_base: string, 97 | start_pts?: number, 98 | start_time?: string, 99 | duration_ts?: number, 100 | duration?: string, 101 | bits_per_raw_sample?: string 102 | } 103 | 104 | export type TFFProbeVideoStream = TBaseStream & { 105 | width: number, 106 | height: number, 107 | coded_width: number, 108 | coded_height: number, 109 | closed_captions: number, 110 | film_grain: number, 111 | has_b_frames: number, 112 | pix_fmt: string, 113 | level: number, 114 | color_range?: string, 115 | color_space?: string, 116 | color_transfer?: string, 117 | color_primaries?: string, 118 | chroma_location: string, 119 | field_order: string, 120 | refs: number, 121 | is_avc: string, 122 | nal_length_size: string, 123 | r_frame_rate: string, 124 | avg_frame_rate: string, 125 | time_base: string, 126 | start_pts: number, 127 | start_time: string, 128 | duration_ts: number, 129 | duration: string, 130 | bit_rate: string, 131 | bits_per_raw_sample: string, 132 | nb_frames: string, 133 | extradata_size: number, 134 | side_data_list: TSideDataList[] 135 | }; 136 | 137 | type TTags = { 138 | major_brand?: string, 139 | minor_version?: string, 140 | compatible_brands?: string, 141 | creation_time?: string, 142 | make?: string, 143 | language?: string, 144 | handler_name?: string, 145 | vendor_id?: string, 146 | encoder: string, 147 | "encoder-eng"?: string, 148 | date?: string, 149 | "date-eng"?: string, 150 | location?: string, 151 | "location-eng"?: string, 152 | model?: string, 153 | [id: string]: unknown 154 | }; 155 | 156 | type TSideDataList = { 157 | side_data_type: string, 158 | displaymatrix: string, 159 | rotation: number 160 | }; 161 | 162 | type TDisposition = { 163 | default: number, 164 | dub: number, 165 | original: number, 166 | comment: number, 167 | lyrics: number, 168 | karaoke: number, 169 | forced: number, 170 | hearing_impaired: number, 171 | visual_impaired: number, 172 | clean_effects: number, 173 | attached_pic: number, 174 | timed_thumbnails: number, 175 | captions: number, 176 | descriptions: number, 177 | metadata: number, 178 | dependent: number, 179 | still_image: number 180 | }; 181 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./ffmpeg-faststart"; 2 | export * from "./ffmpeg-graceful"; 3 | export * from "./ffmpeg-multi-pass"; 4 | export * from "./ffmpeg-playlist"; 5 | export * from "./ffmpeg-simple"; 6 | export * from "./ffmpeg-test"; 7 | export * from "./ffmpeg-volume-detect"; 8 | export * from "./ffmpeg"; 9 | export * from "./ffprobe-simple"; 10 | export * from "./ffprobe"; 11 | export * from "./opts"; 12 | -------------------------------------------------------------------------------- /types/opts.d.ts: -------------------------------------------------------------------------------- 1 | import { TFFmpegTestResponse } from "./ffmpeg-test"; 2 | 3 | export type TBaseOpts = { 4 | docker?: string | { 5 | container: string, 6 | proxy?: string, 7 | replaceArguments?: { 8 | libfaac?: string, 9 | "^/var": string 10 | }, 11 | preprocessFiles?: { 12 | chown?: string, 13 | chmod?: number, 14 | mkdirs?: boolean 15 | }, 16 | postprocessFiles?: { 17 | chown?: string, 18 | chmod?: number, 19 | recordChown?: boolean, 20 | recordChmod?: boolean 21 | } 22 | }, 23 | timeout?: number 24 | }; 25 | 26 | export type TFFmpegOpts = TBaseOpts & { ffmpeg_binary?: string }; 27 | export type TFFProbeOpts = TBaseOpts & { ffprobe_binary?: string }; 28 | 29 | export type TOpts = TFFProbeOpts & TFFmpegOpts & { 30 | test_ffmpeg?: true, 31 | test_info?: TFFmpegTestResponse 32 | }; 33 | --------------------------------------------------------------------------------