├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bin └── videoshow ├── examples ├── audio.js ├── basic.js ├── captions.js ├── logo.js ├── subtitles.js └── transition.js ├── lib ├── copy.js ├── index.js ├── merge.js ├── mstime.js ├── options.js ├── render.js ├── subrip.js ├── substation.js ├── video.js └── videoshow.js ├── package-lock.json ├── package.json └── test ├── cli.js ├── fixtures ├── config.json ├── logo.png ├── norris.gif ├── song.aac ├── song.mp3 ├── song.ogg ├── step_1.png ├── step_2.png ├── step_3.png ├── step_4.png ├── step_5.png ├── subtitles.ass ├── subtitles.srt └── video.jpg ├── subrip.js ├── substation.js └── videoshow.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [Makefile] 12 | charset = utf-8 13 | indent_style = tabs 14 | indent_size = 2 15 | end_of_line = lf 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.sh] 20 | insert_final_newline = false 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | test/.tmp 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | test/.tmp 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | script: echo 0 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tomás Aparicio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MOCHA = ./node_modules/.bin/mocha 2 | 3 | default: all 4 | all: test 5 | test: mocha 6 | 7 | mocha: 8 | $(MOCHA) --harmony --timeout 300000 --reporter spec --ui tdd 9 | 10 | publish: test 11 | git push --tags origin HEAD:master 12 | npm publish 13 | 14 | loc: 15 | wc -l lib/* 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # videoshow [![Code Climate](https://codeclimate.com/github/h2non/videoshow/badges/gpa.svg)](https://codeclimate.com/github/h2non/videoshow) [![NPM](https://img.shields.io/npm/v/videoshow.svg)][npm] 2 | 3 | 4 | 5 | Simple utility for **node.js** to **create straightforward video slideshows based on images** using [ffmpeg](http://ffmpeg.org), with additional features such as **audio**, **subtitles** and **fade in/out transitions** between slides. 6 | 7 | To getting started you can take a look to the [examples](https://github.com/h2non/videoshow/tree/master/examples), [programmatic API](#api) and [command-line](#command-line-interface) interface 8 | 9 | videoshow is used in production rendering thousands of videos per month. 10 | 11 | Click on the image to see an example video generated by `videoshow`: 12 | 13 | 14 | 15 | 16 | 17 | ## Requirements 18 | 19 | - **[ffmpeg](http://ffmpeg.org)** with additional compilation flags `--enable-libass --enable-libmp3lame` 20 | 21 | You can download static builds of ffmpeg from [here](http://johnvansickle.com/ffmpeg/). 22 | 23 | If you want to use videoshow in Heroku, you could use the [ffmpeg2](https://github.com/h2non/heroku-buildpack-ffmpeg2) buildpack 24 | 25 | ## Installation 26 | 27 | ```bash 28 | npm install videoshow 29 | ``` 30 | 31 | For command-line usage, install it as global package: 32 | ```bash 33 | npm install -g videoshow 34 | ``` 35 | 36 | ## Usage 37 | 38 | **NOTE**: images must all have the same dimensions. 39 | 40 | Below you have an example script generating a video based on images and audio. 41 | 42 | Take a look to the [programmatic API](#api) and [examples](examples/) for more usage details. 43 | 44 | ```js 45 | var videoshow = require('videoshow') 46 | 47 | var images = [ 48 | 'step1.jpg', 49 | 'step2.jpg', 50 | 'step3.jpg', 51 | 'step4.jpg' 52 | ] 53 | 54 | var videoOptions = { 55 | fps: 25, 56 | loop: 5, // seconds 57 | transition: true, 58 | transitionDuration: 1, // seconds 59 | videoBitrate: 1024, 60 | videoCodec: 'libx264', 61 | size: '640x?', 62 | audioBitrate: '128k', 63 | audioChannels: 2, 64 | format: 'mp4', 65 | pixelFormat: 'yuv420p' 66 | } 67 | 68 | videoshow(images, videoOptions) 69 | .audio('song.mp3') 70 | .save('video.mp4') 71 | .on('start', function (command) { 72 | console.log('ffmpeg process started:', command) 73 | }) 74 | .on('error', function (err, stdout, stderr) { 75 | console.error('Error:', err) 76 | console.error('ffmpeg stderr:', stderr) 77 | }) 78 | .on('end', function (output) { 79 | console.error('Video created in:', output) 80 | }) 81 | ``` 82 | 83 | ## Command-line interface 84 | 85 | ```bash 86 | $ videoshow --help 87 | ``` 88 | 89 | ```bash 90 | Create video slideshow easily from images 91 | Usage: bin/videoshow [options] 92 | 93 | Options: 94 | --help, -h Show help 95 | --config, -c File path to JSON config file [required] 96 | --audio, -a Optional audio file path 97 | --subtitles, -s Path to .srt subtitles file 98 | --input, -i Add additional input to video 99 | --output, -o Output video file path 100 | --size, -x Video size resolution 101 | --logo, -l Path to logo image 102 | --debug, -d Enable debug mode in error case 103 | 104 | Examples: 105 | bin/videoshow -c config.json video.mp4 106 | bin/videoshow -c config.json --audio song.mp3 video.mp4 107 | bin/videoshow -c config.json --audio song.mp3 --logo logo.png video.mp4 108 | ``` 109 | 110 | Example `config.json` file: 111 | 112 | ```json 113 | { 114 | "output": "video.mp4", 115 | "options": { 116 | "fps": 25, 117 | "loop": 5, 118 | "transition": true, 119 | "transitionDuration": 1, 120 | "videoBitrate": 1024, 121 | "videoCodec": "libx264", 122 | "size": "640x?", 123 | "audioBitrate": "128k", 124 | "audioChannels": 2, 125 | "format": "mp4", 126 | "subtitleStyles": { 127 | "Fontname": "Verdana", 128 | "Fontsize": "26", 129 | "PrimaryColour": "11861244", 130 | "SecondaryColour": "11861244", 131 | "TertiaryColour": "11861244", 132 | "BackColour": "-2147483640", 133 | "Bold": "2", 134 | "Italic": "0", 135 | "BorderStyle": "2", 136 | "Outline": "2", 137 | "Shadow": "3", 138 | "Alignment": "1", 139 | "MarginL": "40", 140 | "MarginR": "60", 141 | "MarginV": "40" 142 | } 143 | }, 144 | "images": [ 145 | "./test/fixtures/step_1.png", 146 | "./test/fixtures/step_2.png", 147 | "./test/fixtures/step_3.png", 148 | "./test/fixtures/step_4.png", 149 | "./test/fixtures/step_5.png" 150 | ] 151 | } 152 | ``` 153 | 154 | ## API 155 | 156 | ### `videoshow(images, [ options ])` 157 | Return: `Videoshow` 158 | 159 | Videoshow constructor. You should pass an `array` or `array` or `array` with the desired images, 160 | and optionally passing the video render `options` object per each image. 161 | 162 | Image formats supported are: `jpg`, `png` or `bmp`. 163 | 164 | ```js 165 | videoshow([ 'image1.jpg', 'image2.jpg', 'image3.jpg']) 166 | .save('video.mp4') 167 | .on('error', function () {}) 168 | .on('end', function () {}) 169 | ``` 170 | 171 | `images` param could be a collection as well: 172 | ```js 173 | videoshow([{ 174 | path: 'image1.jpg', 175 | caption: 'Hello world as video subtitle' 176 | }, { 177 | path: 'image2.jpg', 178 | caption: 'This is a sample subtitle', 179 | loop: 10 // long caption 180 | }]) 181 | .save('video.mp4') 182 | .on('error', function () {}) 183 | .on('end', function () {}) 184 | ``` 185 | 186 | #### Video options 187 | 188 | You can define as option any method name allowed by [fluent-ffmpeg][ffmpeg-api] 189 | 190 | Default options are: 191 | ```js 192 | { 193 | fps: 25, 194 | loop: 5, // seconds 195 | transition: true, 196 | transitionDuration: 1, 197 | captionDelay: 1000, 198 | useSubRipSubtitles: false, 199 | subtitleStyle: null, 200 | videoBitrate: 1024, 201 | videoCodec: 'libx264', 202 | size: '640x?', 203 | audioBitrate: '128k', 204 | audioChannels: 2, 205 | format: 'mp4' 206 | } 207 | ``` 208 | 209 | Options details: 210 | 211 | - **captionDelay** `number` - Miliseconds to delay the show/hide of the caption. Default to `1000` 212 | - **useSubRipSubtitles** `boolean` - Use SubRip subtitles format. It uses by default [SSA/ASS](http://en.wikipedia.org/wiki/SubStation_Alpha). Default `false` 213 | - **subtitleStyle** `object` - SSA/ASS subtitles style. See [substation.js](https://github.com/h2non/videoshow/blob/master/lib/substation.js#L49) and [fixture file](https://github.com/h2non/videoshow/blob/master/test/fixtures/subtitles.ass) for allowed params 214 | 215 | #### Image options 216 | 217 | - **path** `string` - File path to image 218 | - **loop** `number` - Image slide duration in **seconds**. Default to `5` 219 | - **transition** `boolean` - Enable fade in/out transition for the current image 220 | - **transitionDuration** `number` - Fade in/out transition duration in **seconds**. Default to `1` 221 | - **transitionColor** `string` - Fade in/out transition background color. Default to `black`. See [supported colors][ffmpeg-colors] 222 | - **filters** `array` - Add custom ffmpeg video filters to the image slide. 223 | - **disableFadeOut** `boolean` - If transition is enable, disable the fade out. Default `false` 224 | - **disableFadeIn** `boolean` - If transition is enable, disable the fade in. Default `false` 225 | - **caption** `string` - Caption text as subtitle. It allows a limited set of HTML tags. See [Subrip][subrip] 226 | - **captionDelay** `number` - Miliseconds to delay the show/hide of the caption. Default to `1000` 227 | - **captionStart** `number` - Miliseconds to start the caption. Default to `1000` 228 | - **captionEnd** `number` - Miliseconds to remove the caption. Default to `loop - 1000` 229 | - **logo** `string` - Path to logo image. See `logo()` method 230 | 231 | #### `videoshow#image(image)` 232 | 233 | Push an image to the video. You can pass an `string` as path to the image, 234 | or a plain `object` with [image options](#supported-image-options) 235 | 236 | #### `videoshow#audio(path [, params ])` 237 | 238 | Define the audio file path to use. 239 | It supports multiple formats and codecs such as `acc`, `mp3` or `ogg` 240 | 241 | **Supported params**: 242 | 243 | - **delay** `number` - Delay audio start in seconds. Default to `0` seconds 244 | - **fade** `boolean` - Enable audio fade in/out effect. Default `true` 245 | 246 | #### `videoshow#logo(path [, params ])` 247 | 248 | Add a custom image as logo in the left-upper corner by default. You can customize the position by `x/y` axis. 249 | It must be a `png` or `jpeg` image 250 | 251 | **Supported params**: 252 | 253 | - **start** `number` - Video second to show the logo. Default `5` seconds 254 | - **end** `number` - Video second to remove the logo. Default `totalLength - 5` seconds 255 | - **xAxis** `number` - Logo `x` axis position. Default `10` 256 | - **yAxis** `number` - Logo `y` axis position. Default `10` 257 | 258 | #### `videoshow#subtitles(path)` 259 | 260 | Define the [SubRip subtitles][subrip] or [SubStation Alpha](http://en.wikipedia.org/wiki/SubStation_Alpha) (SSA/ASS) 261 | file path to load. It should be a `.str` or `.ass` file respectively 262 | 263 | See [fixtures](https://github.com/h2non/videoshow/blob/master/test/fixtures/subtitles.srt) for examples 264 | 265 | #### `videoshow#save(path)` 266 | Return: `EventEmitter` Alias: `render` 267 | 268 | Render and write in disk the resultant video in the given path 269 | 270 | Supported events for subscription: 271 | 272 | - **start** `cmd` - Fired when ffmpeg process started 273 | - **error** `error, stdout, stderr` - Fired when transcoding error happens 274 | - **progress** `data` - Fired with transcoding progress information 275 | - **codecData** `codec` - Fired when input codec data is available 276 | - **end** `videoPath` - Fired when the process finish successfully 277 | 278 | For more information, see the [ffmpeg docs](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#setting-event-handlers) 279 | 280 | #### `videoshow#input(input)` 281 | 282 | Add input file to video. By default you don't need to call this method 283 | 284 | #### `videoshow#filter(filter)` 285 | 286 | Add a custom video filter to the video. See the [docs](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#videofiltersfilter-add-custom-video-filters) 287 | 288 | #### `videoshow#complexFilter(filter)` 289 | 290 | Add a custom complex filter to the video. See the [docs](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#complexfilterfilters-map-set-complex-filtergraph) 291 | 292 | #### `videoshow#loop(seconds)` 293 | 294 | Default image loop time in seconds. Default to `5` 295 | 296 | #### `videoshow#size(resolution)` 297 | 298 | Video size resolution. Default to `640x?`. 299 | See the [docs](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#sizesize-set-output-frame-size) 300 | 301 | #### `videoshow#aspect(aspect)` 302 | 303 | Video aspect ration. Default autocalculated from video `size` param. 304 | See the [docs](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#aspectaspect-set-output-frame-aspect-ratio) 305 | 306 | #### `videoshow#options(options)` 307 | Alias: `flags` 308 | 309 | Add a set of video output options as command-line flag. 310 | `options` argument should be an array. 311 | See the [docs](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#outputoptionsoption-add-custom-output-options) 312 | 313 | #### `videoshow#option(argument)` 314 | Alias: `flag` 315 | 316 | Add a custom output option as command-line flag to pass to `ffmpeg` 317 | 318 | #### `videoshow#options(arguments)` 319 | Alias: `flags` 320 | 321 | Add multiple output options as command-line flags to pass to `ffmpeg`. 322 | The argument must be an `array` 323 | 324 | ### `videoshow.VERSION` 325 | Type: `string` 326 | 327 | Current package semantic version 328 | 329 | ### `videoshow.ffmpeg` 330 | Type: `function` 331 | 332 | [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg) API constructor 333 | 334 | ## License 335 | 336 | [MIT](http://opensource.org/licenses/MIT) © Tomas Aparicio 337 | 338 | [travis]: http://travis-ci.org/h2non/videoshow 339 | [gemnasium]: https://gemnasium.com/h2non/videoshow 340 | [npm]: http://npmjs.org/package/videoshow 341 | [subrip]: http://en.wikipedia.org/wiki/SubRip#SubRip_text_file_format 342 | [ffmpeg-api]: https://github.com/fluent-ffmpeg/node-fluent-ffmpeg#creating-an-ffmpeg-command 343 | [ffmpeg-colors]: https://www.ffmpeg.org/ffmpeg-utils.html#Color 344 | -------------------------------------------------------------------------------- /bin/videoshow: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs') 4 | var videoshow = require('../') 5 | var version = require('../package.json').version 6 | 7 | var yargs = require('yargs') 8 | var argv = yargs 9 | .usage('Create video slideshows easily from images with audio, subtitles and transitions\nUsage: $0 [options]') 10 | .example('$0 -c config.json video.mp4', '') 11 | .help('help').alias('help', 'h') 12 | .version(version) 13 | .alias('v', 'version') 14 | .options({ 15 | config: { 16 | alias: 'c', 17 | description: 'File path to JSON config file', 18 | required: true 19 | }, 20 | audio: { 21 | alias: 'a', 22 | description: 'Optional audio file path' 23 | }, 24 | subtitles: { 25 | alias: 's', 26 | description: 'Path to .srt subtitles file' 27 | }, 28 | input: { 29 | alias: 'i', 30 | description: 'Add additional input to video' 31 | }, 32 | output: { 33 | alias: 'o', 34 | description: 'Output video file path' 35 | }, 36 | size: { 37 | alias: 'x', 38 | description: 'Video size resolution' 39 | }, 40 | logo: { 41 | alias: 'l', 42 | description: 'Path to logo image' 43 | }, 44 | debug: { 45 | alias: 'd', 46 | description: 'Enable debug mode in error case', 47 | type: 'boolean' 48 | } 49 | }) 50 | .argv 51 | 52 | try { 53 | render(options()) 54 | } catch (e) { 55 | error(e) 56 | } 57 | 58 | function render(opts) { 59 | var output = argv.output || opts.output || getOutput() 60 | var audio = argv.audio || opts.audio 61 | var subtitles = argv.subtitles || opts.subtitles 62 | var size = argv.size || opts.size 63 | var logo = argv.logo || opts.logo 64 | var loop = argv.loop || opts.loop 65 | 66 | var video = videoshow(opts.images, opts.options) 67 | 68 | if (audio) video.audio(audio) 69 | if (subtitles) video.subtitles(subtitles) 70 | if (size) video.size(size) 71 | if (logo) video.logo(logo) 72 | if (loop) video.loop(loop) 73 | 74 | video 75 | .save(output) 76 | .on('error', error) 77 | .on('start', function (command) { 78 | if (argv.debug) { 79 | echo('ffmpeg process started:', command) 80 | } 81 | }) 82 | .on('end', function () { 83 | echo('Video created:', output) 84 | }) 85 | } 86 | 87 | function getOutput() { 88 | var output = argv._.pop() 89 | if (output && output[0] !== '-') { 90 | return output 91 | } 92 | return 'video.mp4' 93 | } 94 | 95 | function options() { 96 | var opts = {} 97 | var config = JSON.parse(fs.readFileSync(argv.config)) 98 | if (config && (config.options || config.images)) { 99 | opts = config 100 | } 101 | return opts 102 | } 103 | 104 | function error(msg) { 105 | if (argv.debug && msg.stack) { 106 | console.error(msg.stack || msg) 107 | } else { 108 | console.error(msg.message || msg) 109 | } 110 | process.exitCode = 1 111 | } 112 | 113 | function echo() { 114 | console.log.apply(console, arguments) 115 | } 116 | -------------------------------------------------------------------------------- /examples/audio.js: -------------------------------------------------------------------------------- 1 | var videoshow = require('../') 2 | 3 | var audio = __dirname + '/../test/fixtures/song.mp3' 4 | 5 | var audioParams = { 6 | fade: true, 7 | delay: 2 // seconds 8 | } 9 | 10 | var images = [ 11 | __dirname + '/../test/fixtures/step_1.png', 12 | __dirname + '/../test/fixtures/step_2.png', 13 | __dirname + '/../test/fixtures/step_3.png', 14 | __dirname + '/../test/fixtures/step_4.png', 15 | __dirname + '/../test/fixtures/step_5.png' 16 | ] 17 | 18 | videoshow(images) 19 | .audio(audio, audioParams) 20 | .save('video.mp4') 21 | .on('start', function (command) { 22 | console.log('ffmpeg process started:', command) 23 | }) 24 | .on('error', function (err) { 25 | console.error('Error:', err) 26 | }) 27 | .on('end', function (output) { 28 | console.log('Video created in:', output) 29 | }) 30 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | var videoshow = require('../') 2 | 3 | var images = [ 4 | __dirname + '/../test/fixtures/step_1.png', 5 | __dirname + '/../test/fixtures/step_2.png', 6 | __dirname + '/../test/fixtures/step_3.png', 7 | __dirname + '/../test/fixtures/step_4.png', 8 | __dirname + '/../test/fixtures/step_5.png' 9 | ] 10 | 11 | videoshow(images) 12 | .save('video.mp4') 13 | .on('start', function (command) { 14 | console.log('ffmpeg process started:', command) 15 | }) 16 | .on('error', function (err) { 17 | console.error('Error:', err) 18 | }) 19 | .on('end', function (output) { 20 | console.log('Video created in:', output) 21 | }) 22 | -------------------------------------------------------------------------------- /examples/captions.js: -------------------------------------------------------------------------------- 1 | var videoshow = require('../') 2 | 3 | var audio = __dirname + '/../test/fixtures/song.mp3' 4 | 5 | var options = { 6 | loop: 5, 7 | captionDelay: 350, 8 | transition: true, 9 | useSubRipSubtitles: false, // Use ASS/SSA subtitles instead 10 | subtitleStyle: { 11 | Fontname: 'Verdana', 12 | Fontsize: '26', 13 | PrimaryColour: '11861244', 14 | SecondaryColour: '11861244', 15 | TertiaryColour: '11861244', 16 | BackColour: '-2147483640', 17 | Bold: '2', 18 | Italic: '0', 19 | BorderStyle: '2', 20 | Outline: '2', 21 | Shadow: '3', 22 | Alignment: '1', // left, middle, right 23 | MarginL: '40', 24 | MarginR: '60', 25 | MarginV: '40' 26 | } 27 | } 28 | 29 | var images = [ 30 | { 31 | path: __dirname + '/../test/fixtures/step_1.png', 32 | caption: 'This is a sample subtitle' 33 | }, { 34 | path: __dirname + '/../test/fixtures/step_2.png', 35 | caption: 'Another sample text', 36 | loop: 5 37 | }, { 38 | path: __dirname + '/../test/fixtures/step_3.png', 39 | caption: 'Fast caption', 40 | captionStart: 2, 41 | captionEnd: 3 42 | }, { 43 | path: __dirname + '/../test/fixtures/step_4.png', 44 | loop: 3 45 | }, { 46 | path: __dirname + '/../test/fixtures/step_5.png', 47 | caption: 'Bye bye' 48 | } 49 | ] 50 | 51 | videoshow(images, options) 52 | .audio(audio) 53 | .save('video.mp4') 54 | .on('start', function (command) { 55 | console.log('ffmpeg process started:', command) 56 | }) 57 | .on('error', function (err) { 58 | console.error('Error:', err) 59 | }) 60 | .on('end', function (output) { 61 | console.log('Video created in:', output) 62 | }) 63 | -------------------------------------------------------------------------------- /examples/logo.js: -------------------------------------------------------------------------------- 1 | var videoshow = require('../') 2 | 3 | var subtitles = __dirname + '/../test/fixtures/subtitles.srt' 4 | var audio = __dirname + '/../test/fixtures/song.mp3' 5 | var logo = __dirname + '/../test/fixtures/logo.png' 6 | var logoParams = { 7 | start: 5, 8 | end: 20, 9 | xAxis: 20, 10 | yAxis: 20 11 | } 12 | 13 | var images = [ 14 | __dirname + '/../test/fixtures/step_1.png', 15 | __dirname + '/../test/fixtures/step_2.png', 16 | __dirname + '/../test/fixtures/step_3.png', 17 | __dirname + '/../test/fixtures/step_4.png', 18 | __dirname + '/../test/fixtures/step_5.png' 19 | ] 20 | 21 | videoshow(images) 22 | .subtitles(subtitles) 23 | .audio(audio) 24 | .logo(logo, logoParams) 25 | .save('video.mp4') 26 | .on('start', function (command) { 27 | console.log('ffmpeg process started:', command) 28 | }) 29 | .on('error', function (err) { 30 | console.error('Error:', err) 31 | }) 32 | .on('end', function (output) { 33 | console.log('Video created in:', output) 34 | }) 35 | -------------------------------------------------------------------------------- /examples/subtitles.js: -------------------------------------------------------------------------------- 1 | var videoshow = require('../') 2 | 3 | var subtitles = __dirname + '/../test/fixtures/subtitles.ass' 4 | //var subtitles = __dirname + '/../test/fixtures/subtitles.srt' 5 | var audio = __dirname + '/../test/fixtures/song.mp3' 6 | 7 | var images = [ 8 | __dirname + '/../test/fixtures/step_1.png', 9 | __dirname + '/../test/fixtures/step_2.png', 10 | __dirname + '/../test/fixtures/step_3.png', 11 | __dirname + '/../test/fixtures/step_4.png', 12 | __dirname + '/../test/fixtures/step_5.png' 13 | ] 14 | 15 | videoshow(images) 16 | .subtitles(subtitles) 17 | .audio(audio) 18 | .save('video.mp4') 19 | .on('start', function (command) { 20 | console.log('ffmpeg process started:', command) 21 | }) 22 | .on('error', function (err) { 23 | console.error('Error:', err) 24 | }) 25 | .on('end', function (output) { 26 | console.log('Video created in:', output) 27 | }) 28 | -------------------------------------------------------------------------------- /examples/transition.js: -------------------------------------------------------------------------------- 1 | var videoshow = require('../') 2 | 3 | var audio = __dirname + '/../test/fixtures/song.mp3' 4 | 5 | var options = { 6 | transition: true 7 | } 8 | 9 | var images = [ 10 | { 11 | path: __dirname + '/../test/fixtures/step_1.png', 12 | disableFadeOut: true, 13 | loop: 2 14 | }, { 15 | path: __dirname + '/../test/fixtures/step_2.png', 16 | disableFadeIn: true, 17 | loop: 5 18 | }, { 19 | path: __dirname + '/../test/fixtures/step_3.png', 20 | transitionColor: '0xFF66C7' 21 | }, { 22 | path: __dirname + '/../test/fixtures/step_4.png', 23 | transition: false, 24 | transitionColor: 'red' 25 | }, { 26 | path: __dirname + '/../test/fixtures/step_5.png', 27 | transition: false 28 | } 29 | ] 30 | 31 | videoshow(images, options) 32 | .audio(audio) 33 | .save('video.mp4') 34 | .on('start', function (command) { 35 | console.log('ffmpeg process started:', command) 36 | }) 37 | .on('error', function (err) { 38 | console.error('Error:', err) 39 | }) 40 | .on('end', function (output) { 41 | console.log('Video created in:', output) 42 | }) 43 | -------------------------------------------------------------------------------- /lib/copy.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | module.exports = function copy(src, dest, cb) { 4 | fs.createReadStream(src) 5 | .on('error', cb) 6 | .on('end', cb) 7 | .pipe(fs.createWriteStream(dest)) 8 | } 9 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require('fluent-ffmpeg') 2 | var Videoshow = require('./videoshow') 3 | var pkg = require('../package.json') 4 | 5 | module.exports = videoshow 6 | 7 | function videoshow(images, options) { 8 | return new Videoshow(images, options) 9 | } 10 | 11 | videoshow.VERSION = pkg.version 12 | videoshow.ffmpeg = ffmpeg 13 | -------------------------------------------------------------------------------- /lib/merge.js: -------------------------------------------------------------------------------- 1 | var os = require('os') 2 | var ffmpeg = require('fluent-ffmpeg') 3 | var applyVideoOptions = require('./options').applyVideo 4 | 5 | module.exports = merge 6 | 7 | function merge(parts, output, options) { 8 | parts = parts.slice() 9 | var video = ffmpeg(parts.shift()) 10 | 11 | parts.forEach(function (part) { 12 | video.input(part) 13 | }) 14 | 15 | if (options) { 16 | applyVideoOptions(video, options) 17 | } 18 | 19 | video.mergeToFile(output, os.tmpdir()) 20 | 21 | return video 22 | } 23 | -------------------------------------------------------------------------------- /lib/mstime.js: -------------------------------------------------------------------------------- 1 | var measures = [ 3600000, 60000, 1000 ] 2 | 3 | var msTime = module.exports = function (val, leadingZeros) { 4 | leadingZeros = leadingZeros || 2 5 | 6 | var time = measures.map(function (measure) { 7 | var res = (val / measure >> 0).toString() 8 | if (res.length < 2) { 9 | res = '0' + res 10 | } 11 | val %= measure 12 | return res 13 | }) 14 | 15 | var ms = Math.floor(val / 10).toString() 16 | for (var i = 0, l = ms.length; i <= leadingZeros - l; i += 1) { 17 | ms = '0' + ms 18 | } 19 | 20 | return time.join(':') + ',' + ms 21 | } 22 | 23 | msTime.substation = function (val) { 24 | var time = msTime(val, 1) 25 | return time.replace(',', '.').slice(1) 26 | } 27 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | var merge = require('lodash.merge') 2 | 3 | var defaults = exports.defaults = { 4 | fps: 25, 5 | loop: 5, 6 | transition: true, 7 | captionDelay: 1000, 8 | transitionDuration: 1, 9 | transitionColor: 'black', 10 | videoBitrate: 1024, 11 | videoCodec: 'libx264', 12 | size: '640x?', 13 | audioBitrate: '128k', 14 | audioChannels: 2, 15 | format: 'mp4', 16 | useSubripSubtitles: false, 17 | subtitleStyle: null 18 | } 19 | 20 | exports.define = function (options) { 21 | return merge({}, defaults, options) 22 | } 23 | 24 | exports.applyVideo = function (video, options) { 25 | Object.keys(options).forEach(function (key) { 26 | var method = video[key] 27 | var arg = options[key] 28 | 29 | if (typeof method === 'function') { 30 | method = method.bind(video) 31 | if (arg === true) { 32 | method() 33 | } else if (Array.isArray(arg)) { 34 | if (arg.length) { 35 | method(arg) 36 | } 37 | } else if (arg != null) { 38 | method(arg) 39 | } 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /lib/render.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var fw = require('fw') 3 | var os = require('os') 4 | var path = require('path') 5 | var uuid = require('lil-uuid') 6 | var union = require('lodash.merge') 7 | var ffmpeg = require('fluent-ffmpeg') 8 | var EventEmitter = require('events').EventEmitter 9 | 10 | var subrip = require('./subrip') 11 | var substation = require('./substation') 12 | var copy = require('./copy') 13 | var video = require('./video') 14 | var merge = require('./merge') 15 | var options = require('./options') 16 | var defaults = options.defaults 17 | var applyVideoOptions = options.applyVideo 18 | 19 | module.exports = render 20 | 21 | function render(videoshow) { 22 | var bus = new EventEmitter 23 | 24 | process.nextTick(function () { 25 | processVideo(videoshow, bus) 26 | }) 27 | 28 | return bus 29 | } 30 | 31 | function processVideo(videoshow, bus) { 32 | var imageJobs = convertImages(videoshow) 33 | var merge = mergeParts(videoshow, bus) 34 | 35 | fw.series(imageJobs, merge) 36 | } 37 | 38 | function convertImages(videoshow) { 39 | var params = options.define(videoshow.params) 40 | 41 | return videoshow.images.map(function (image) { 42 | var output = randomName() 43 | return function (done) { 44 | video(image, params, output) 45 | .on('error', done) 46 | .on('end', function () { 47 | done(null, output) 48 | }) 49 | } 50 | }) 51 | } 52 | 53 | function mergeParts(videoshow, bus) { 54 | var forwardEvent = proxyEvent(bus) 55 | var output = videoshow.output 56 | 57 | return function (err, images) { 58 | if (err) return bus.emit('error', err) 59 | 60 | var cleanup = mergeHandler(images, output, forwardEvent) 61 | var ender = cleanup('end') 62 | var error = cleanup('error') 63 | var options = videoshow.videoParams 64 | 65 | merge(images, output, options) 66 | .on('start', forwardEvent('start')) 67 | .on('codecData', forwardEvent('codecData')) 68 | .on('progress', forwardEvent('progress')) 69 | .on('error', error) 70 | .on('end', end) 71 | 72 | function end() { 73 | var options = null 74 | if (videoshow.audioFile) { 75 | options = audioOptions(videoshow) 76 | } 77 | renderVideo(videoshow, options, forwardEvent, handler) 78 | } 79 | 80 | function handler(err) { 81 | if (err) return error(err) 82 | ender() 83 | } 84 | } 85 | } 86 | 87 | function audioOptions(videoshow) { 88 | var length = calculateVideoLength(videoshow) 89 | var params = videoshow.audioParams 90 | 91 | var options = [ 92 | '-map 0:0', 93 | '-map 1:0', 94 | '-t ' + length 95 | ] 96 | 97 | if (params) { 98 | if (params.fade) { 99 | options.push('-af afade=t=in:ss=0:st=' + (params.delay || 0) + ':d=3') 100 | options.push('-af afade=t=out:st=' + (length - 3) + ':d=3') 101 | } 102 | } 103 | 104 | return options 105 | } 106 | 107 | function renderVideo(videoshow, options, forwardEvent, cb) { 108 | var tempfile = randomName() 109 | var output = videoshow.output 110 | var audio = videoshow.audioFile 111 | var videoParams = videoshow.videoParams 112 | var audioParams = videoshow.audioParams 113 | var params = videoshow.params 114 | var videoOptions = union({}, params, videoParams) 115 | var subtitles = getImageSubtitles(videoshow) || videoshow.subtitlesPath 116 | options = options || [] 117 | 118 | copy(output, tempfile, function (err) { 119 | if (err) return cb(err) 120 | 121 | var video = ffmpeg(tempfile) 122 | videoOptions.loop = null 123 | applyVideoOptions(video, videoOptions) 124 | 125 | if (audio) { 126 | video.input(audio) 127 | if (audioParams.delay) { 128 | video.inputOptions('-itsoffset ' + audioParams.delay) 129 | } 130 | } 131 | 132 | videoOptions.inputs.forEach(function (input) { 133 | video.input(input) 134 | }) 135 | 136 | if (subtitles) { 137 | options.push('-vf subtitles=\'' + subtitles + '\'') 138 | } 139 | 140 | if (videoOptions.pixelFormat) { 141 | options.push("-pix_fmt " + videoOptions.pixelFormat); 142 | } 143 | 144 | video 145 | .outputOptions(options) 146 | .on('error', clean(end)) 147 | .on('end', clean(end)) 148 | .on('start', forwardEvent('start')) 149 | .on('progress', forwardEvent('progress')) 150 | .on('codecData', forwardEvent('codecData')) 151 | .save(output) 152 | }) 153 | 154 | function end(err) { 155 | if (subtitles !== videoshow.subtitlesPath) { 156 | remove(subtitles) 157 | } 158 | 159 | if (!err && videoOptions.logo) { 160 | return addLogo(cb) 161 | } 162 | 163 | cb.apply(null, arguments) 164 | } 165 | 166 | function addLogo(cb) { 167 | copy(output, tempfile, function (err) { 168 | if (err) return cb(err) 169 | 170 | var logo = ffmpeg(tempfile) 171 | applyVideoOptions(logo, videoOptions) 172 | logo 173 | .input(videoOptions.logo) 174 | .complexFilter(logoFilter(videoshow)) 175 | .on('error', clean(cb)) 176 | .on('end', clean(cb)) 177 | .on('start', forwardEvent('start')) 178 | .on('progress', forwardEvent('progress')) 179 | .on('codecData', forwardEvent('codecData')) 180 | .save(output) 181 | }) 182 | } 183 | 184 | function clean(cb) { 185 | return function (err, stdout, stderr) { 186 | remove(tempfile, function () { 187 | if (err) { 188 | remove(output) 189 | } 190 | cb(err, stdout, stderr) 191 | }) 192 | } 193 | } 194 | } 195 | 196 | function logoFilter(videoshow) { 197 | var logo = videoshow.params.logo 198 | var params = videoshow.params.logoParams || {} 199 | var length = calculateVideoLength(videoshow) 200 | 201 | var xAxis = params.xAxis || 10 202 | var yAxis = params.yAxis || 10 203 | var logoStart = params.start || (length > 30 ? 10 : length * 0.20).toFixed(0) 204 | var logoEnd = params.end || (length > 30 ? length - 10 : length * 0.80).toFixed(0) 205 | 206 | if (logoEnd < 0) { 207 | logoEnd = length + logoEnd 208 | } 209 | 210 | return [ 211 | '[0:v][1:v]', 212 | 'overlay=', xAxis, ':', yAxis, ':', 213 | 'enable=between', 214 | '(t\\,', logoStart, '\\,', logoEnd, ')' 215 | ].join('') 216 | } 217 | 218 | function getImageSubtitles(videoshow) { 219 | var filepath = null 220 | var params = options.define(videoshow.params) 221 | var extension = '.ass' 222 | 223 | var length = 0 224 | var subtitles = videoshow.images 225 | .map(function (image, index) { 226 | var offset = calculateOffsetDelay(index) 227 | if (image.caption) { 228 | return renderCaption(image, length++, offset, params) 229 | } 230 | }) 231 | .filter(function (caption) { 232 | return caption != null 233 | }) 234 | 235 | if (!subtitles.length) { 236 | return 237 | } 238 | 239 | if (params.useSubRipSubtitles) { 240 | extension = '.srt' 241 | subtitles = subrip.stringify(subtitles) 242 | } else { 243 | subtitles = substation.stringify(subtitles, params.subtitleStyle) 244 | } 245 | 246 | filepath = randomName() + extension 247 | fs.writeFileSync(filepath, subtitles) 248 | 249 | if (os.platform() === 'win32') { 250 | filepath = filepath.replace(/\\/g, "\\\\") 251 | filepath = filepath.replace(/:/g, "\\:") 252 | } 253 | 254 | return filepath 255 | 256 | function calculateOffsetDelay(index) { 257 | return videoshow.images.slice(0, index).reduce(function (prev, current) { 258 | return prev + ((current.loop || params.loop) * 1000) 259 | }, 0) 260 | } 261 | } 262 | 263 | function renderCaption(image, index, offset, params) { 264 | var loop = (+image.loop || +params.loop) * 1000 265 | var delay = +image.captionDelay || +params.captionDelay || 1000 266 | var startTime = offset + (+image.captionStart || 1000) 267 | var endTime = offset + (+image.captionEnd || (loop - delay)) 268 | 269 | var subtitleMeta = { 270 | id: index, 271 | startTime: startTime, 272 | endTime: endTime, 273 | text: image.caption 274 | } 275 | 276 | return subtitleMeta 277 | } 278 | 279 | function mergeHandler(images, output, forwardEvent) { 280 | return function cleanup(event) { 281 | var forward = forwardEvent(event) 282 | return function handler(err) { 283 | var args = arguments 284 | removeFiles(images, function () { 285 | if (event === 'error') { 286 | forward.apply(null, args) 287 | } else { 288 | forward(output) 289 | } 290 | }) 291 | } 292 | } 293 | } 294 | 295 | function proxyEvent(bus) { 296 | return function (event) { 297 | return function (value) { 298 | bus.emit(event, value) 299 | } 300 | } 301 | } 302 | 303 | function removeFiles(files, cb) { 304 | fw.parallel(files.map(function (file) { 305 | return function (done) { 306 | remove(file, done) 307 | } 308 | }), cb) 309 | } 310 | 311 | function calculateVideoLength(videoshow) { 312 | var images = videoshow.images 313 | return images.reduce(function (acc, image) { 314 | return acc + (image.loop || videoshow.params.loop || defaults.loop) 315 | }, 0) 316 | } 317 | 318 | function remove(file, cb) { 319 | fs.unlink(file, cb || function () {}) 320 | } 321 | 322 | function randomName() { 323 | return path.join(os.tmpdir(), 'videoshow-' + uuid()) 324 | } 325 | -------------------------------------------------------------------------------- /lib/subrip.js: -------------------------------------------------------------------------------- 1 | var msTime = require('./mstime') 2 | 3 | exports.stringify = function (data) { 4 | var buf = data.map(function (s) { 5 | var track = [] 6 | 7 | if (!isNaN(s.startTime) && !isNaN(s.endTime)) { 8 | s.startTime = msTime(+s.startTime) 9 | s.endTime = msTime(+s.endTime) 10 | } 11 | 12 | track.push(s.id) 13 | track.push(s.startTime + ' --> ' + s.endTime) 14 | track.push(s.text) 15 | 16 | return track.join('\n') 17 | }) 18 | 19 | return buf.join('\n\n') 20 | } 21 | -------------------------------------------------------------------------------- /lib/substation.js: -------------------------------------------------------------------------------- 1 | var substation = require('ass-stringify') 2 | var merge = require('lodash.merge') 3 | var msTime = require('./mstime') 4 | 5 | var captionParams = { 6 | 'Marked': 'Marked=0', 7 | 'Start': '0:00:01.18', 8 | 'End': '0:00:06.85', 9 | 'Style': 'DefaultVCD', 10 | 'Name': 'NTP', 11 | 'MarginL': '0000', 12 | 'MarginR': '0000', 13 | 'MarginV': '0000', 14 | 'Effect': null, 15 | 'Text': null 16 | } 17 | 18 | var defaultParams = { 19 | 'Script Info': { 20 | 'Title': 'Videoshow video', 21 | 'ScriptType': 'v4', 22 | 'Collisions': 'Normal', 23 | 'PlayResY': '600', 24 | 'PlayDepth': '0', 25 | 'Timer': '100,0000' 26 | }, 27 | 'Empty': {}, 28 | 'V4 Styles': { 29 | 'Format': [ 30 | 'Name', 31 | 'Fontname', 32 | 'Fontsize', 33 | 'PrimaryColour', 34 | 'SecondaryColour', 35 | 'TertiaryColour', 36 | 'BackColour', 37 | 'Bold', 38 | 'Italic', 39 | 'BorderStyle', 40 | 'Outline', 41 | 'Shadow', 42 | 'Alignment', 43 | 'MarginL', 44 | 'MarginR', 45 | 'MarginV', 46 | 'AlphaLevel', 47 | 'Encoding' 48 | ], 49 | 'Style': { 50 | 'Name': 'DefaultVCD', 51 | 'Fontname': 'Arial', 52 | 'Fontsize': '28', 53 | 'PrimaryColour': '11861244', 54 | 'SecondaryColour': '11861244', 55 | 'TertiaryColour': '11861244', 56 | 'BackColour': '-2147483640', 57 | 'Bold': '-1', 58 | 'Italic': '0', 59 | 'BorderStyle': '1', 60 | 'Outline': '1', 61 | 'Shadow': '2', 62 | 'Alignment': '2', 63 | 'MarginL': '30', 64 | 'MarginR': '30', 65 | 'MarginV': '30', 66 | 'AlphaLevel': '0', 67 | 'Encoding': '0' 68 | } 69 | }, 70 | 'Events': { 71 | 'Format': [ 72 | 'Marked', 73 | 'Start', 74 | 'End', 75 | 'Style', 76 | 'Name', 77 | 'MarginL', 78 | 'MarginR', 79 | 'MarginV', 80 | 'Effect', 81 | 'Text' 82 | ] 83 | } 84 | } 85 | 86 | exports.stringify = function (captions, styles) { 87 | var params = mergeParams(styles) 88 | 89 | captions.forEach(function (caption) { 90 | var subtitle = merge({}, captionParams, { 91 | Start: msTime.substation(+caption.startTime), 92 | End: msTime.substation(+caption.endTime), 93 | Text: caption.text 94 | }) 95 | 96 | var body = params[params.length - 1].body 97 | body.push({ 98 | key: 'Dialogue', 99 | value: subtitle 100 | }) 101 | }) 102 | 103 | return substation(params) 104 | } 105 | 106 | function mergeParams(styles) { 107 | var params = merge({}, defaultParams) 108 | merge(params['V4 Styles'].Style, styles) 109 | 110 | return Object.keys(params).map(function (key) { 111 | var body = params[key] 112 | var map = { section: key } 113 | 114 | map.body = Object.keys(body).map(function (prop) { 115 | return { 116 | key: prop, 117 | value: body[prop] 118 | } 119 | }) 120 | 121 | return map 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /lib/video.js: -------------------------------------------------------------------------------- 1 | var ffmpeg = require('fluent-ffmpeg') 2 | var options = require('./options') 3 | 4 | module.exports = video 5 | 6 | function video(image, params, output) { 7 | var video = ffmpeg(image.path) 8 | params = options.define(params) 9 | 10 | if (image.loop) { 11 | params.loop = image.loop 12 | } 13 | 14 | options.applyVideo(video, params) 15 | 16 | if (image.filters) { 17 | video.videoFilters(image.filters) 18 | } 19 | 20 | if ((params.transition && image.transition !== false) || image.transition) { 21 | video.videoFilters(transitionFilter(image, params)) 22 | } 23 | 24 | video.save(output) 25 | 26 | return video 27 | } 28 | 29 | function transitionFilter(image, params) { 30 | var options = [] 31 | var duration = image.transitionDuration || params.transitionDuration 32 | var color = image.transitionColor || params.transitionColor 33 | var loop = image.loop || params.loop 34 | 35 | if (!image.disableFadeIn) { 36 | options.push('fade=t=in:st=0:d=' + duration + ':color=' + color) 37 | } 38 | 39 | if (!image.disableFadeOut) { 40 | options.push('fade=out:st=' + (loop - duration) + ':d=' + duration + ':color=' + color) 41 | } 42 | 43 | return options 44 | } 45 | -------------------------------------------------------------------------------- /lib/videoshow.js: -------------------------------------------------------------------------------- 1 | var union = require('lodash.merge') 2 | var render = require('./render') 3 | var options = require('./options') 4 | 5 | module.exports = Videoshow 6 | 7 | function Videoshow(images, params) { 8 | this.params = params || {} 9 | this.audioParams = { fade: true } 10 | this.videoParams = videoParams() 11 | this.images = mapImages(images) 12 | } 13 | 14 | Videoshow.prototype.size = function (size) { 15 | this.params.size = size 16 | return this 17 | } 18 | 19 | Videoshow.prototype.aspect = function (aspect) { 20 | this.params.aspect = aspect 21 | return this 22 | } 23 | 24 | Videoshow.prototype.loop = function (seconds) { 25 | this.params.loop = seconds 26 | return this 27 | } 28 | 29 | Videoshow.prototype.image = function (image) { 30 | this.images.push(mapImages(image)) 31 | return this 32 | } 33 | 34 | Videoshow.prototype.audio = function (path, params) { 35 | this.audioFile = path 36 | union(this.audioParams, params) 37 | return this 38 | } 39 | 40 | Videoshow.prototype.logo = function (path, params) { 41 | this.params.logo = path 42 | this.params.logoParams = params 43 | return this 44 | } 45 | 46 | Videoshow.prototype.subtitles = function (path) { 47 | this.subtitlesPath = path 48 | return this 49 | } 50 | 51 | Videoshow.prototype.filter = function (filter) { 52 | this.videoParams.videoFilters.push(filter) 53 | return this 54 | } 55 | 56 | Videoshow.prototype.complexFilter = function (filter) { 57 | this.videoParams.complexFilter.push(filter) 58 | return this 59 | } 60 | 61 | Videoshow.prototype.audioFilter = function (filter) { 62 | this.videoParams.audioFilters.push(filter) 63 | return this 64 | } 65 | 66 | Videoshow.prototype.input = function (input) { 67 | this.videoParams.inputs.push(input) 68 | return this 69 | } 70 | 71 | Videoshow.prototype.flag = 72 | Videoshow.prototype.option = function (option) { 73 | this.videoParams.outputOptions.push(option) 74 | return this 75 | } 76 | 77 | Videoshow.prototype.flags = 78 | Videoshow.prototype.options = function (options) { 79 | options.forEach(this.option.bind(this)) 80 | return this 81 | } 82 | 83 | Videoshow.prototype.save = 84 | Videoshow.prototype.render = function (output) { 85 | this.output = output 86 | return render(this) 87 | } 88 | 89 | function videoParams() { 90 | return { 91 | inputs: [], 92 | videoFilters: [], 93 | audioFilters: [], 94 | complexFilter: [], 95 | outputOptions: [] 96 | } 97 | } 98 | 99 | function mapImages(obj) { 100 | if (typeof obj === 'string') { 101 | return { path: obj } 102 | } 103 | 104 | if (Array.isArray(obj)) { 105 | return obj.map(mapImages) 106 | } 107 | 108 | if (obj && typeof obj === 'object') { 109 | return obj 110 | } 111 | 112 | throw new TypeError('image must be a string or an array') 113 | } 114 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videoshow", 3 | "version": "0.1.12", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-colors": { 8 | "version": "3.2.3", 9 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", 10 | "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", 11 | "dev": true 12 | }, 13 | "ansi-regex": { 14 | "version": "2.1.1", 15 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 16 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 17 | }, 18 | "ansi-styles": { 19 | "version": "3.2.1", 20 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 21 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 22 | "dev": true, 23 | "requires": { 24 | "color-convert": "^1.9.0" 25 | } 26 | }, 27 | "argparse": { 28 | "version": "1.0.10", 29 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 30 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 31 | "dev": true, 32 | "requires": { 33 | "sprintf-js": "~1.0.2" 34 | } 35 | }, 36 | "ass-stringify": { 37 | "version": "0.1.3", 38 | "resolved": "https://registry.npmjs.org/ass-stringify/-/ass-stringify-0.1.3.tgz", 39 | "integrity": "sha1-M97zXpVyaC8TM+sa4ezhkt5TErk=", 40 | "requires": { 41 | "pick-values": "^1.0.0" 42 | } 43 | }, 44 | "assertion-error": { 45 | "version": "1.0.0", 46 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", 47 | "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", 48 | "dev": true 49 | }, 50 | "async": { 51 | "version": "3.2.4", 52 | "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", 53 | "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" 54 | }, 55 | "balanced-match": { 56 | "version": "1.0.0", 57 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 58 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 59 | "dev": true 60 | }, 61 | "brace-expansion": { 62 | "version": "1.1.11", 63 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 64 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 65 | "dev": true, 66 | "requires": { 67 | "balanced-match": "^1.0.0", 68 | "concat-map": "0.0.1" 69 | } 70 | }, 71 | "browser-stdout": { 72 | "version": "1.3.1", 73 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 74 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 75 | "dev": true 76 | }, 77 | "camelcase": { 78 | "version": "2.1.1", 79 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", 80 | "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" 81 | }, 82 | "chai": { 83 | "version": "1.10.0", 84 | "resolved": "https://registry.npmjs.org/chai/-/chai-1.10.0.tgz", 85 | "integrity": "sha1-5AMcyHZURhp1lD5aNatG6vOcHrk=", 86 | "dev": true, 87 | "requires": { 88 | "assertion-error": "1.0.0", 89 | "deep-eql": "0.1.3" 90 | } 91 | }, 92 | "chalk": { 93 | "version": "2.4.2", 94 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 95 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 96 | "dev": true, 97 | "requires": { 98 | "ansi-styles": "^3.2.1", 99 | "escape-string-regexp": "^1.0.5", 100 | "supports-color": "^5.3.0" 101 | }, 102 | "dependencies": { 103 | "supports-color": { 104 | "version": "5.5.0", 105 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 106 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 107 | "dev": true, 108 | "requires": { 109 | "has-flag": "^3.0.0" 110 | } 111 | } 112 | } 113 | }, 114 | "cliui": { 115 | "version": "3.2.0", 116 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", 117 | "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", 118 | "requires": { 119 | "string-width": "^1.0.1", 120 | "strip-ansi": "^3.0.1", 121 | "wrap-ansi": "^2.0.0" 122 | } 123 | }, 124 | "code-point-at": { 125 | "version": "1.1.0", 126 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 127 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 128 | }, 129 | "color-convert": { 130 | "version": "1.9.3", 131 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 132 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 133 | "dev": true, 134 | "requires": { 135 | "color-name": "1.1.3" 136 | } 137 | }, 138 | "color-name": { 139 | "version": "1.1.3", 140 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 141 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 142 | "dev": true 143 | }, 144 | "concat-map": { 145 | "version": "0.0.1", 146 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 147 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 148 | "dev": true 149 | }, 150 | "cross-spawn": { 151 | "version": "6.0.5", 152 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 153 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 154 | "dev": true, 155 | "requires": { 156 | "nice-try": "^1.0.4", 157 | "path-key": "^2.0.1", 158 | "semver": "^5.5.0", 159 | "shebang-command": "^1.2.0", 160 | "which": "^1.2.9" 161 | } 162 | }, 163 | "debug": { 164 | "version": "3.2.6", 165 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 166 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 167 | "dev": true, 168 | "requires": { 169 | "ms": "^2.1.1" 170 | } 171 | }, 172 | "decamelize": { 173 | "version": "1.2.0", 174 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 175 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" 176 | }, 177 | "deep-eql": { 178 | "version": "0.1.3", 179 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", 180 | "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", 181 | "dev": true, 182 | "requires": { 183 | "type-detect": "0.1.1" 184 | } 185 | }, 186 | "define-properties": { 187 | "version": "1.1.3", 188 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 189 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 190 | "dev": true, 191 | "requires": { 192 | "object-keys": "^1.0.12" 193 | } 194 | }, 195 | "diff": { 196 | "version": "3.5.0", 197 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 198 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 199 | "dev": true 200 | }, 201 | "emoji-regex": { 202 | "version": "7.0.3", 203 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 204 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 205 | "dev": true 206 | }, 207 | "end-of-stream": { 208 | "version": "1.4.1", 209 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", 210 | "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", 211 | "dev": true, 212 | "requires": { 213 | "once": "^1.4.0" 214 | } 215 | }, 216 | "es-abstract": { 217 | "version": "1.14.1", 218 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.14.1.tgz", 219 | "integrity": "sha512-cp/Tb1oA/rh2X7vqeSOvM+TSo3UkJLX70eNihgVEvnzwAgikjkTFr/QVgRCaxjm0knCNQzNoxxxcw2zO2LJdZA==", 220 | "dev": true, 221 | "requires": { 222 | "es-to-primitive": "^1.2.0", 223 | "function-bind": "^1.1.1", 224 | "has": "^1.0.3", 225 | "has-symbols": "^1.0.0", 226 | "is-callable": "^1.1.4", 227 | "is-regex": "^1.0.4", 228 | "object-inspect": "^1.6.0", 229 | "object-keys": "^1.1.1", 230 | "string.prototype.trimleft": "^2.0.0", 231 | "string.prototype.trimright": "^2.0.0" 232 | } 233 | }, 234 | "es-to-primitive": { 235 | "version": "1.2.0", 236 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", 237 | "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", 238 | "dev": true, 239 | "requires": { 240 | "is-callable": "^1.1.4", 241 | "is-date-object": "^1.0.1", 242 | "is-symbol": "^1.0.2" 243 | } 244 | }, 245 | "escape-string-regexp": { 246 | "version": "1.0.5", 247 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 248 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 249 | "dev": true 250 | }, 251 | "esprima": { 252 | "version": "4.0.1", 253 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 254 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 255 | "dev": true 256 | }, 257 | "execa": { 258 | "version": "1.0.0", 259 | "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", 260 | "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", 261 | "dev": true, 262 | "requires": { 263 | "cross-spawn": "^6.0.0", 264 | "get-stream": "^4.0.0", 265 | "is-stream": "^1.1.0", 266 | "npm-run-path": "^2.0.0", 267 | "p-finally": "^1.0.0", 268 | "signal-exit": "^3.0.0", 269 | "strip-eof": "^1.0.0" 270 | } 271 | }, 272 | "find-up": { 273 | "version": "3.0.0", 274 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", 275 | "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", 276 | "dev": true, 277 | "requires": { 278 | "locate-path": "^3.0.0" 279 | } 280 | }, 281 | "flat": { 282 | "version": "4.1.0", 283 | "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", 284 | "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", 285 | "dev": true, 286 | "requires": { 287 | "is-buffer": "~2.0.3" 288 | } 289 | }, 290 | "fluent-ffmpeg": { 291 | "version": "2.1.2", 292 | "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", 293 | "integrity": "sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=", 294 | "requires": { 295 | "async": ">=0.2.9", 296 | "which": "^1.1.1" 297 | } 298 | }, 299 | "fs.realpath": { 300 | "version": "1.0.0", 301 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 302 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 303 | "dev": true 304 | }, 305 | "function-bind": { 306 | "version": "1.1.1", 307 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 308 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 309 | "dev": true 310 | }, 311 | "fw": { 312 | "version": "0.1.2", 313 | "resolved": "https://registry.npmjs.org/fw/-/fw-0.1.2.tgz", 314 | "integrity": "sha1-NST2eT0W6DvAEk6UUILBMJO9eZo=" 315 | }, 316 | "get-caller-file": { 317 | "version": "2.0.5", 318 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 319 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 320 | "dev": true 321 | }, 322 | "get-stream": { 323 | "version": "4.1.0", 324 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", 325 | "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", 326 | "dev": true, 327 | "requires": { 328 | "pump": "^3.0.0" 329 | } 330 | }, 331 | "glob": { 332 | "version": "7.1.3", 333 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 334 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 335 | "dev": true, 336 | "requires": { 337 | "fs.realpath": "^1.0.0", 338 | "inflight": "^1.0.4", 339 | "inherits": "2", 340 | "minimatch": "^3.0.4", 341 | "once": "^1.3.0", 342 | "path-is-absolute": "^1.0.0" 343 | } 344 | }, 345 | "growl": { 346 | "version": "1.10.5", 347 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 348 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 349 | "dev": true 350 | }, 351 | "has": { 352 | "version": "1.0.3", 353 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 354 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 355 | "dev": true, 356 | "requires": { 357 | "function-bind": "^1.1.1" 358 | } 359 | }, 360 | "has-flag": { 361 | "version": "3.0.0", 362 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 363 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 364 | "dev": true 365 | }, 366 | "has-symbols": { 367 | "version": "1.0.0", 368 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", 369 | "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", 370 | "dev": true 371 | }, 372 | "he": { 373 | "version": "1.2.0", 374 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 375 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 376 | "dev": true 377 | }, 378 | "inflight": { 379 | "version": "1.0.6", 380 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 381 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 382 | "dev": true, 383 | "requires": { 384 | "once": "^1.3.0", 385 | "wrappy": "1" 386 | } 387 | }, 388 | "inherits": { 389 | "version": "2.0.4", 390 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 391 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 392 | "dev": true 393 | }, 394 | "invert-kv": { 395 | "version": "1.0.0", 396 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", 397 | "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" 398 | }, 399 | "is-buffer": { 400 | "version": "2.0.3", 401 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", 402 | "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", 403 | "dev": true 404 | }, 405 | "is-callable": { 406 | "version": "1.1.4", 407 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", 408 | "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", 409 | "dev": true 410 | }, 411 | "is-date-object": { 412 | "version": "1.0.1", 413 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 414 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 415 | "dev": true 416 | }, 417 | "is-fullwidth-code-point": { 418 | "version": "1.0.0", 419 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 420 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 421 | "requires": { 422 | "number-is-nan": "^1.0.0" 423 | } 424 | }, 425 | "is-regex": { 426 | "version": "1.0.4", 427 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 428 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 429 | "dev": true, 430 | "requires": { 431 | "has": "^1.0.1" 432 | } 433 | }, 434 | "is-stream": { 435 | "version": "1.1.0", 436 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 437 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", 438 | "dev": true 439 | }, 440 | "is-symbol": { 441 | "version": "1.0.2", 442 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", 443 | "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", 444 | "dev": true, 445 | "requires": { 446 | "has-symbols": "^1.0.0" 447 | } 448 | }, 449 | "isexe": { 450 | "version": "2.0.0", 451 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 452 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 453 | }, 454 | "js-yaml": { 455 | "version": "3.13.1", 456 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 457 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 458 | "dev": true, 459 | "requires": { 460 | "argparse": "^1.0.7", 461 | "esprima": "^4.0.0" 462 | } 463 | }, 464 | "lcid": { 465 | "version": "1.0.0", 466 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", 467 | "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", 468 | "requires": { 469 | "invert-kv": "^1.0.0" 470 | } 471 | }, 472 | "lil-uuid": { 473 | "version": "0.1.1", 474 | "resolved": "https://registry.npmjs.org/lil-uuid/-/lil-uuid-0.1.1.tgz", 475 | "integrity": "sha1-+e3PI/AOQr9D8PhD2Y2LU/M0HxY=" 476 | }, 477 | "locate-path": { 478 | "version": "3.0.0", 479 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", 480 | "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", 481 | "dev": true, 482 | "requires": { 483 | "p-locate": "^3.0.0", 484 | "path-exists": "^3.0.0" 485 | } 486 | }, 487 | "lodash": { 488 | "version": "4.17.21", 489 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 490 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", 491 | "dev": true 492 | }, 493 | "lodash.merge": { 494 | "version": "4.6.2", 495 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 496 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" 497 | }, 498 | "log-symbols": { 499 | "version": "2.2.0", 500 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", 501 | "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", 502 | "dev": true, 503 | "requires": { 504 | "chalk": "^2.0.1" 505 | } 506 | }, 507 | "map-age-cleaner": { 508 | "version": "0.1.3", 509 | "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", 510 | "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", 511 | "dev": true, 512 | "requires": { 513 | "p-defer": "^1.0.0" 514 | } 515 | }, 516 | "mem": { 517 | "version": "4.3.0", 518 | "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", 519 | "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", 520 | "dev": true, 521 | "requires": { 522 | "map-age-cleaner": "^0.1.1", 523 | "mimic-fn": "^2.0.0", 524 | "p-is-promise": "^2.0.0" 525 | } 526 | }, 527 | "mimic-fn": { 528 | "version": "2.1.0", 529 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 530 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 531 | "dev": true 532 | }, 533 | "minimatch": { 534 | "version": "3.0.4", 535 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 536 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 537 | "dev": true, 538 | "requires": { 539 | "brace-expansion": "^1.1.7" 540 | } 541 | }, 542 | "minimist": { 543 | "version": "0.0.8", 544 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 545 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 546 | "dev": true 547 | }, 548 | "mkdirp": { 549 | "version": "0.5.1", 550 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 551 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 552 | "dev": true, 553 | "requires": { 554 | "minimist": "0.0.8" 555 | } 556 | }, 557 | "mocha": { 558 | "version": "6.2.0", 559 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.0.tgz", 560 | "integrity": "sha512-qwfFgY+7EKAAUAdv7VYMZQknI7YJSGesxHyhn6qD52DV8UcSZs5XwCifcZGMVIE4a5fbmhvbotxC0DLQ0oKohQ==", 561 | "dev": true, 562 | "requires": { 563 | "ansi-colors": "3.2.3", 564 | "browser-stdout": "1.3.1", 565 | "debug": "3.2.6", 566 | "diff": "3.5.0", 567 | "escape-string-regexp": "1.0.5", 568 | "find-up": "3.0.0", 569 | "glob": "7.1.3", 570 | "growl": "1.10.5", 571 | "he": "1.2.0", 572 | "js-yaml": "3.13.1", 573 | "log-symbols": "2.2.0", 574 | "minimatch": "3.0.4", 575 | "mkdirp": "0.5.1", 576 | "ms": "2.1.1", 577 | "node-environment-flags": "1.0.5", 578 | "object.assign": "4.1.0", 579 | "strip-json-comments": "2.0.1", 580 | "supports-color": "6.0.0", 581 | "which": "1.3.1", 582 | "wide-align": "1.1.3", 583 | "yargs": "13.2.2", 584 | "yargs-parser": "13.0.0", 585 | "yargs-unparser": "1.5.0" 586 | }, 587 | "dependencies": { 588 | "ansi-regex": { 589 | "version": "3.0.0", 590 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 591 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 592 | "dev": true 593 | }, 594 | "cliui": { 595 | "version": "4.1.0", 596 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", 597 | "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", 598 | "dev": true, 599 | "requires": { 600 | "string-width": "^2.1.1", 601 | "strip-ansi": "^4.0.0", 602 | "wrap-ansi": "^2.0.0" 603 | }, 604 | "dependencies": { 605 | "string-width": { 606 | "version": "2.1.1", 607 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 608 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 609 | "dev": true, 610 | "requires": { 611 | "is-fullwidth-code-point": "^2.0.0", 612 | "strip-ansi": "^4.0.0" 613 | } 614 | } 615 | } 616 | }, 617 | "invert-kv": { 618 | "version": "2.0.0", 619 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", 620 | "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", 621 | "dev": true 622 | }, 623 | "is-fullwidth-code-point": { 624 | "version": "2.0.0", 625 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 626 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 627 | "dev": true 628 | }, 629 | "lcid": { 630 | "version": "2.0.0", 631 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", 632 | "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", 633 | "dev": true, 634 | "requires": { 635 | "invert-kv": "^2.0.0" 636 | } 637 | }, 638 | "os-locale": { 639 | "version": "3.1.0", 640 | "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", 641 | "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", 642 | "dev": true, 643 | "requires": { 644 | "execa": "^1.0.0", 645 | "lcid": "^2.0.0", 646 | "mem": "^4.0.0" 647 | } 648 | }, 649 | "string-width": { 650 | "version": "3.1.0", 651 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 652 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 653 | "dev": true, 654 | "requires": { 655 | "emoji-regex": "^7.0.1", 656 | "is-fullwidth-code-point": "^2.0.0", 657 | "strip-ansi": "^5.1.0" 658 | }, 659 | "dependencies": { 660 | "ansi-regex": { 661 | "version": "4.1.0", 662 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 663 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 664 | "dev": true 665 | }, 666 | "strip-ansi": { 667 | "version": "5.2.0", 668 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 669 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 670 | "dev": true, 671 | "requires": { 672 | "ansi-regex": "^4.1.0" 673 | } 674 | } 675 | } 676 | }, 677 | "strip-ansi": { 678 | "version": "4.0.0", 679 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 680 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 681 | "dev": true, 682 | "requires": { 683 | "ansi-regex": "^3.0.0" 684 | } 685 | }, 686 | "y18n": { 687 | "version": "4.0.1", 688 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", 689 | "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", 690 | "dev": true 691 | }, 692 | "yargs": { 693 | "version": "13.2.2", 694 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", 695 | "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", 696 | "dev": true, 697 | "requires": { 698 | "cliui": "^4.0.0", 699 | "find-up": "^3.0.0", 700 | "get-caller-file": "^2.0.1", 701 | "os-locale": "^3.1.0", 702 | "require-directory": "^2.1.1", 703 | "require-main-filename": "^2.0.0", 704 | "set-blocking": "^2.0.0", 705 | "string-width": "^3.0.0", 706 | "which-module": "^2.0.0", 707 | "y18n": "^4.0.0", 708 | "yargs-parser": "^13.0.0" 709 | } 710 | } 711 | } 712 | }, 713 | "ms": { 714 | "version": "2.1.1", 715 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 716 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", 717 | "dev": true 718 | }, 719 | "nice-try": { 720 | "version": "1.0.5", 721 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 722 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", 723 | "dev": true 724 | }, 725 | "node-environment-flags": { 726 | "version": "1.0.5", 727 | "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", 728 | "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", 729 | "dev": true, 730 | "requires": { 731 | "object.getownpropertydescriptors": "^2.0.3", 732 | "semver": "^5.7.0" 733 | } 734 | }, 735 | "npm-run-path": { 736 | "version": "2.0.2", 737 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", 738 | "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", 739 | "dev": true, 740 | "requires": { 741 | "path-key": "^2.0.0" 742 | } 743 | }, 744 | "number-is-nan": { 745 | "version": "1.0.1", 746 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 747 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 748 | }, 749 | "object-inspect": { 750 | "version": "1.6.0", 751 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", 752 | "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", 753 | "dev": true 754 | }, 755 | "object-keys": { 756 | "version": "1.1.1", 757 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 758 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 759 | "dev": true 760 | }, 761 | "object.assign": { 762 | "version": "4.1.0", 763 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", 764 | "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", 765 | "dev": true, 766 | "requires": { 767 | "define-properties": "^1.1.2", 768 | "function-bind": "^1.1.1", 769 | "has-symbols": "^1.0.0", 770 | "object-keys": "^1.0.11" 771 | } 772 | }, 773 | "object.getownpropertydescriptors": { 774 | "version": "2.0.3", 775 | "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", 776 | "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", 777 | "dev": true, 778 | "requires": { 779 | "define-properties": "^1.1.2", 780 | "es-abstract": "^1.5.1" 781 | } 782 | }, 783 | "once": { 784 | "version": "1.4.0", 785 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 786 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 787 | "dev": true, 788 | "requires": { 789 | "wrappy": "1" 790 | } 791 | }, 792 | "os-locale": { 793 | "version": "1.4.0", 794 | "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", 795 | "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", 796 | "requires": { 797 | "lcid": "^1.0.0" 798 | } 799 | }, 800 | "p-defer": { 801 | "version": "1.0.0", 802 | "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", 803 | "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", 804 | "dev": true 805 | }, 806 | "p-finally": { 807 | "version": "1.0.0", 808 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", 809 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", 810 | "dev": true 811 | }, 812 | "p-is-promise": { 813 | "version": "2.1.0", 814 | "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", 815 | "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", 816 | "dev": true 817 | }, 818 | "p-limit": { 819 | "version": "2.2.1", 820 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", 821 | "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", 822 | "dev": true, 823 | "requires": { 824 | "p-try": "^2.0.0" 825 | } 826 | }, 827 | "p-locate": { 828 | "version": "3.0.0", 829 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", 830 | "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", 831 | "dev": true, 832 | "requires": { 833 | "p-limit": "^2.0.0" 834 | } 835 | }, 836 | "p-try": { 837 | "version": "2.2.0", 838 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 839 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 840 | "dev": true 841 | }, 842 | "path-exists": { 843 | "version": "3.0.0", 844 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 845 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 846 | "dev": true 847 | }, 848 | "path-is-absolute": { 849 | "version": "1.0.1", 850 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 851 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 852 | "dev": true 853 | }, 854 | "path-key": { 855 | "version": "2.0.1", 856 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 857 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 858 | "dev": true 859 | }, 860 | "pick-values": { 861 | "version": "1.0.1", 862 | "resolved": "https://registry.npmjs.org/pick-values/-/pick-values-1.0.1.tgz", 863 | "integrity": "sha1-Z7LXc05ZPrW6CCpUoEd1YON0JU0=" 864 | }, 865 | "pump": { 866 | "version": "3.0.0", 867 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 868 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 869 | "dev": true, 870 | "requires": { 871 | "end-of-stream": "^1.1.0", 872 | "once": "^1.3.1" 873 | } 874 | }, 875 | "require-directory": { 876 | "version": "2.1.1", 877 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 878 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 879 | "dev": true 880 | }, 881 | "require-main-filename": { 882 | "version": "2.0.0", 883 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", 884 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", 885 | "dev": true 886 | }, 887 | "rimraf": { 888 | "version": "2.7.1", 889 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 890 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 891 | "dev": true, 892 | "requires": { 893 | "glob": "^7.1.3" 894 | }, 895 | "dependencies": { 896 | "glob": { 897 | "version": "7.1.4", 898 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", 899 | "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", 900 | "dev": true, 901 | "requires": { 902 | "fs.realpath": "^1.0.0", 903 | "inflight": "^1.0.4", 904 | "inherits": "2", 905 | "minimatch": "^3.0.4", 906 | "once": "^1.3.0", 907 | "path-is-absolute": "^1.0.0" 908 | } 909 | }, 910 | "minimatch": { 911 | "version": "3.0.4", 912 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 913 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 914 | "dev": true, 915 | "requires": { 916 | "brace-expansion": "^1.1.7" 917 | } 918 | } 919 | } 920 | }, 921 | "semver": { 922 | "version": "5.7.1", 923 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 924 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 925 | "dev": true 926 | }, 927 | "set-blocking": { 928 | "version": "2.0.0", 929 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 930 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", 931 | "dev": true 932 | }, 933 | "shebang-command": { 934 | "version": "1.2.0", 935 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 936 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 937 | "dev": true, 938 | "requires": { 939 | "shebang-regex": "^1.0.0" 940 | } 941 | }, 942 | "shebang-regex": { 943 | "version": "1.0.0", 944 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 945 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 946 | "dev": true 947 | }, 948 | "signal-exit": { 949 | "version": "3.0.2", 950 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 951 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 952 | "dev": true 953 | }, 954 | "sprintf-js": { 955 | "version": "1.0.3", 956 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 957 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 958 | "dev": true 959 | }, 960 | "string-width": { 961 | "version": "1.0.2", 962 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 963 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 964 | "requires": { 965 | "code-point-at": "^1.0.0", 966 | "is-fullwidth-code-point": "^1.0.0", 967 | "strip-ansi": "^3.0.0" 968 | } 969 | }, 970 | "string.prototype.trimleft": { 971 | "version": "2.0.0", 972 | "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.0.0.tgz", 973 | "integrity": "sha1-aLaqjhYsaoDnbjqKDC50cYbicf8=", 974 | "dev": true, 975 | "requires": { 976 | "define-properties": "^1.1.2", 977 | "function-bind": "^1.0.2" 978 | } 979 | }, 980 | "string.prototype.trimright": { 981 | "version": "2.0.0", 982 | "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.0.0.tgz", 983 | "integrity": "sha1-q0pW2AKgH75yk+EehPJNyBZGYd0=", 984 | "dev": true, 985 | "requires": { 986 | "define-properties": "^1.1.2", 987 | "function-bind": "^1.0.2" 988 | } 989 | }, 990 | "strip-ansi": { 991 | "version": "3.0.1", 992 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 993 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 994 | "requires": { 995 | "ansi-regex": "^2.0.0" 996 | } 997 | }, 998 | "strip-eof": { 999 | "version": "1.0.0", 1000 | "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", 1001 | "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", 1002 | "dev": true 1003 | }, 1004 | "strip-json-comments": { 1005 | "version": "2.0.1", 1006 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1007 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 1008 | "dev": true 1009 | }, 1010 | "supports-color": { 1011 | "version": "6.0.0", 1012 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", 1013 | "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", 1014 | "dev": true, 1015 | "requires": { 1016 | "has-flag": "^3.0.0" 1017 | } 1018 | }, 1019 | "type-detect": { 1020 | "version": "0.1.1", 1021 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", 1022 | "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", 1023 | "dev": true 1024 | }, 1025 | "which": { 1026 | "version": "1.3.1", 1027 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1028 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1029 | "requires": { 1030 | "isexe": "^2.0.0" 1031 | } 1032 | }, 1033 | "which-module": { 1034 | "version": "2.0.0", 1035 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 1036 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", 1037 | "dev": true 1038 | }, 1039 | "wide-align": { 1040 | "version": "1.1.3", 1041 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 1042 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 1043 | "dev": true, 1044 | "requires": { 1045 | "string-width": "^1.0.2 || 2" 1046 | } 1047 | }, 1048 | "window-size": { 1049 | "version": "0.1.4", 1050 | "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", 1051 | "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" 1052 | }, 1053 | "wrap-ansi": { 1054 | "version": "2.1.0", 1055 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", 1056 | "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", 1057 | "requires": { 1058 | "string-width": "^1.0.1", 1059 | "strip-ansi": "^3.0.1" 1060 | } 1061 | }, 1062 | "wrappy": { 1063 | "version": "1.0.2", 1064 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1065 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1066 | "dev": true 1067 | }, 1068 | "y18n": { 1069 | "version": "3.2.2", 1070 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", 1071 | "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" 1072 | }, 1073 | "yargs": { 1074 | "version": "3.32.0", 1075 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", 1076 | "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", 1077 | "requires": { 1078 | "camelcase": "^2.0.1", 1079 | "cliui": "^3.0.3", 1080 | "decamelize": "^1.1.1", 1081 | "os-locale": "^1.4.0", 1082 | "string-width": "^1.0.1", 1083 | "window-size": "^0.1.4", 1084 | "y18n": "^3.2.0" 1085 | } 1086 | }, 1087 | "yargs-parser": { 1088 | "version": "13.0.0", 1089 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", 1090 | "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", 1091 | "dev": true, 1092 | "requires": { 1093 | "camelcase": "^5.0.0", 1094 | "decamelize": "^1.2.0" 1095 | }, 1096 | "dependencies": { 1097 | "camelcase": { 1098 | "version": "5.3.1", 1099 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 1100 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", 1101 | "dev": true 1102 | } 1103 | } 1104 | }, 1105 | "yargs-unparser": { 1106 | "version": "1.5.0", 1107 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", 1108 | "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", 1109 | "dev": true, 1110 | "requires": { 1111 | "flat": "^4.1.0", 1112 | "lodash": "^4.17.11", 1113 | "yargs": "^12.0.5" 1114 | }, 1115 | "dependencies": { 1116 | "ansi-regex": { 1117 | "version": "3.0.0", 1118 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 1119 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 1120 | "dev": true 1121 | }, 1122 | "camelcase": { 1123 | "version": "5.3.1", 1124 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 1125 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", 1126 | "dev": true 1127 | }, 1128 | "cliui": { 1129 | "version": "4.1.0", 1130 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", 1131 | "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", 1132 | "dev": true, 1133 | "requires": { 1134 | "string-width": "^2.1.1", 1135 | "strip-ansi": "^4.0.0", 1136 | "wrap-ansi": "^2.0.0" 1137 | } 1138 | }, 1139 | "get-caller-file": { 1140 | "version": "1.0.3", 1141 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", 1142 | "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", 1143 | "dev": true 1144 | }, 1145 | "invert-kv": { 1146 | "version": "2.0.0", 1147 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", 1148 | "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", 1149 | "dev": true 1150 | }, 1151 | "is-fullwidth-code-point": { 1152 | "version": "2.0.0", 1153 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 1154 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 1155 | "dev": true 1156 | }, 1157 | "lcid": { 1158 | "version": "2.0.0", 1159 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", 1160 | "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", 1161 | "dev": true, 1162 | "requires": { 1163 | "invert-kv": "^2.0.0" 1164 | } 1165 | }, 1166 | "os-locale": { 1167 | "version": "3.1.0", 1168 | "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", 1169 | "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", 1170 | "dev": true, 1171 | "requires": { 1172 | "execa": "^1.0.0", 1173 | "lcid": "^2.0.0", 1174 | "mem": "^4.0.0" 1175 | } 1176 | }, 1177 | "require-main-filename": { 1178 | "version": "1.0.1", 1179 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", 1180 | "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", 1181 | "dev": true 1182 | }, 1183 | "string-width": { 1184 | "version": "2.1.1", 1185 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 1186 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 1187 | "dev": true, 1188 | "requires": { 1189 | "is-fullwidth-code-point": "^2.0.0", 1190 | "strip-ansi": "^4.0.0" 1191 | } 1192 | }, 1193 | "strip-ansi": { 1194 | "version": "4.0.0", 1195 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 1196 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 1197 | "dev": true, 1198 | "requires": { 1199 | "ansi-regex": "^3.0.0" 1200 | } 1201 | }, 1202 | "yargs": { 1203 | "version": "12.0.5", 1204 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", 1205 | "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", 1206 | "dev": true, 1207 | "requires": { 1208 | "cliui": "^4.0.0", 1209 | "decamelize": "^1.2.0", 1210 | "find-up": "^3.0.0", 1211 | "get-caller-file": "^1.0.1", 1212 | "os-locale": "^3.0.0", 1213 | "require-directory": "^2.1.1", 1214 | "require-main-filename": "^1.0.1", 1215 | "set-blocking": "^2.0.0", 1216 | "string-width": "^2.0.0", 1217 | "which-module": "^2.0.0", 1218 | "y18n": "^3.2.1 || ^4.0.0", 1219 | "yargs-parser": "^11.1.1" 1220 | } 1221 | }, 1222 | "yargs-parser": { 1223 | "version": "11.1.1", 1224 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", 1225 | "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", 1226 | "dev": true, 1227 | "requires": { 1228 | "camelcase": "^5.0.0", 1229 | "decamelize": "^1.2.0" 1230 | } 1231 | } 1232 | } 1233 | } 1234 | } 1235 | } 1236 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videoshow", 3 | "version": "0.1.12", 4 | "description": "Simple command-line and programmatic interface to create videos slideshows from images using ffmpeg", 5 | "repository": "h2non/videoshow", 6 | "author": "Tomas Aparicio", 7 | "license": "MIT", 8 | "main": "lib", 9 | "directories": { 10 | "lib": "./lib" 11 | }, 12 | "bin": { 13 | "videoshow": "./bin/videoshow" 14 | }, 15 | "scripts": { 16 | "test": "make test" 17 | }, 18 | "keywords": [ 19 | "video", 20 | "photo", 21 | "slide", 22 | "slides", 23 | "image", 24 | "show", 25 | "ffmpeg", 26 | "mpeg", 27 | "avi", 28 | "xvid", 29 | "compose", 30 | "slider", 31 | "transition", 32 | "photos", 33 | "moviemaker", 34 | "maker", 35 | "movie", 36 | "slideshow", 37 | "slideshows", 38 | "animoto" 39 | ], 40 | "dependencies": { 41 | "ass-stringify": "^0.1.1", 42 | "fluent-ffmpeg": "^2.0.0", 43 | "fw": "^0.1.2", 44 | "lil-uuid": "^0.1.0", 45 | "lodash.merge": "^4.6.2", 46 | "yargs": "^3.5.4" 47 | }, 48 | "devDependencies": { 49 | "chai": "^1.10.0", 50 | "mocha": "^6.2.0", 51 | "rimraf": "^2.2.8" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var expect = require('chai').expect 3 | var exec = require('child_process').exec 4 | 5 | suite('command-line', function () { 6 | var output = 'test/.tmp/video.mp4' 7 | 8 | function clean() { 9 | fs.unlinkSync(output) 10 | } 11 | 12 | test('config', function (done) { 13 | var args = '--config test/fixtures/config.json --output ' + output 14 | 15 | exec('bin/videoshow ' + args, 16 | function (error, stdout, stderr) { 17 | expect(stdout).to.match(/video/i) 18 | expect(stderr).to.be.empty 19 | expect(fs.existsSync(output)).to.be.true 20 | clean() 21 | done() 22 | }) 23 | }) 24 | 25 | test('audio', function (done) { 26 | var args = '--config test/fixtures/config.json --audio test/fixtures/song.ogg ' + output 27 | 28 | exec('bin/videoshow ' + args, 29 | function (error, stdout, stderr) { 30 | expect(stdout).to.match(/video/i) 31 | expect(stderr).to.be.empty 32 | expect(fs.existsSync(output)).to.be.true 33 | clean() 34 | done() 35 | }) 36 | }) 37 | 38 | test('logo', function (done) { 39 | var args = '--config test/fixtures/config.json --logo test/fixtures/logo.png ' + output 40 | 41 | exec('bin/videoshow ' + args, 42 | function (error, stdout, stderr) { 43 | expect(stdout).to.match(/video/i) 44 | expect(stderr).to.be.empty 45 | expect(fs.existsSync(output)).to.be.true 46 | clean() 47 | done() 48 | }) 49 | }) 50 | 51 | test('size', function (done) { 52 | var args = '--config test/fixtures/config.json --size 320x? ' + output 53 | 54 | exec('bin/videoshow ' + args, 55 | function (error, stdout, stderr) { 56 | expect(stdout).to.match(/video/i) 57 | expect(stderr).to.be.empty 58 | expect(fs.existsSync(output)).to.be.true 59 | clean() 60 | done() 61 | }) 62 | }) 63 | 64 | test('debug', function (done) { 65 | var args = '--config test/fixtures/config.json --debug ' + output 66 | 67 | exec('bin/videoshow ' + args, 68 | function (error, stdout, stderr) { 69 | expect(stdout).to.match(/video/i) 70 | expect(stdout).to.match(/ffmpeg/i) 71 | expect(stderr).to.be.empty 72 | expect(fs.existsSync(output)).to.be.true 73 | clean() 74 | done() 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /test/fixtures/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "fps": 25, 4 | "loop": 5, 5 | "transition": true, 6 | "transitionDuration": 1, 7 | "videoBitrate": 1024, 8 | "videoCodec": "libx264", 9 | "size": "640x?", 10 | "audioBitrate": "128k", 11 | "audioChannels": 2, 12 | "format": "mp4" 13 | }, 14 | "images": [ 15 | "./test/fixtures/step_1.png", 16 | "./test/fixtures/step_2.png", 17 | "./test/fixtures/step_3.png", 18 | "./test/fixtures/step_4.png", 19 | "./test/fixtures/step_5.png" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/logo.png -------------------------------------------------------------------------------- /test/fixtures/norris.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/norris.gif -------------------------------------------------------------------------------- /test/fixtures/song.aac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/song.aac -------------------------------------------------------------------------------- /test/fixtures/song.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/song.mp3 -------------------------------------------------------------------------------- /test/fixtures/song.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/song.ogg -------------------------------------------------------------------------------- /test/fixtures/step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/step_1.png -------------------------------------------------------------------------------- /test/fixtures/step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/step_2.png -------------------------------------------------------------------------------- /test/fixtures/step_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/step_3.png -------------------------------------------------------------------------------- /test/fixtures/step_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/step_4.png -------------------------------------------------------------------------------- /test/fixtures/step_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/step_5.png -------------------------------------------------------------------------------- /test/fixtures/subtitles.ass: -------------------------------------------------------------------------------- 1 | [Script Info] 2 | ; This is a Sub Station Alpha v4 script. 3 | ; For Sub Station Alpha info and downloads, 4 | ; go to http://www.eswat.demon.co.uk/ 5 | Title: Neon Genesis Evangelion - Episode 26 6 | Original Script: RoRo 7 | Collisions: Normal 8 | PlayDepth: 0 9 | Timer: 100,0000 10 | 11 | [Empty] 12 | 13 | [V4 Styles] 14 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding 15 | Style: DefaultVCD,Verdana,12,11861244,11861244,11861244,-2147483640,-1,0,1,1,2,2,30,30,30,0,0 16 | 17 | [Events] 18 | Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 19 | Dialogue: Marked=0,0:00:01.18,0:00:06.85,DefaultVCD,NTP,0000,0000,0000,,{\pos(400,570)}Like an angel with pity on nobody 20 | Dialogue: Marked=0,0:00:07.00,0:00:12.00,DefaultVCD,NTP,0000,0000,0000,,Hey this is another text Hey this is another text Hey this is another text Hey this is another text Hey this is another text,Hey this is another text,Hey this is another text 21 | Dialogue: Marked=0,0:00:13.00,0:00:16.00,DefaultVCD,NTP,0000,0000,0000,,Hello world 22 | -------------------------------------------------------------------------------- /test/fixtures/subtitles.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,394 --> 00:00:05,031 3 | Hello World using 4 | “subtitles” 5 | 6 | 2 7 | 00:00:06,000 --> 00:00:09,099 8 | Long slide step description testing: Lorem ipsum ad his scripta blandit partiendo, eum fastidii accumsan euripidis in, eum liber hendrerit an. 9 | 10 | 3 11 | 00:00:10,000 --> 00:00:14,099 12 | Step 3 description"norris" 13 | 14 | 4 15 | 00:00:15,000 --> 00:00:19,099 16 | Step 4 description"jackie" 17 | -------------------------------------------------------------------------------- /test/fixtures/video.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/videoshow/5dbda65d1b931b3d9839d347ef76fd2cad089cb4/test/fixtures/video.jpg -------------------------------------------------------------------------------- /test/subrip.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var subrip = require('../lib/subrip') 3 | 4 | suite('subrip', function () { 5 | suite('stringify', function () { 6 | test('single', function () { 7 | var subtitle = { 8 | id: 1, 9 | startTime: 1000, 10 | endTime: 3000, 11 | text: 'Hello World' 12 | } 13 | 14 | var lines = subrip.stringify([ subtitle ]).split('\n') 15 | expect(lines.shift()).to.be.equal('1') 16 | expect(lines.shift()).to.be.equal('00:00:01,000 --> 00:00:03,000') 17 | expect(lines.shift()).to.be.equal('Hello World') 18 | }) 19 | 20 | test('multiline', function () { 21 | var subtitles = [{ 22 | id: 1, 23 | startTime: 1000, 24 | endTime: 3000, 25 | text: 'Hello World' 26 | }, { 27 | id: 2, 28 | startTime: 2000, 29 | endTime: 5000, 30 | text: 'Hello World' 31 | }] 32 | 33 | var lines = subrip.stringify(subtitles).split('\n') 34 | expect(lines.shift()).to.be.equal('1') 35 | expect(lines.shift()).to.be.equal('00:00:01,000 --> 00:00:03,000') 36 | expect(lines.shift()).to.be.equal('Hello World') 37 | 38 | lines.shift() 39 | 40 | expect(lines.shift()).to.be.equal('2') 41 | expect(lines.shift()).to.be.equal('00:00:02,000 --> 00:00:05,000') 42 | expect(lines.shift()).to.be.equal('Hello World') 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/substation.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var substation = require('../lib/substation') 3 | 4 | suite('substation', function () { 5 | suite('stringify', function () { 6 | test('single', function () { 7 | var subtitles = [ 8 | { 9 | id: 1, 10 | startTime: 1000, 11 | endTime: 3000, 12 | text: 'Hello World' 13 | }, 14 | { 15 | id: 2, 16 | startTime: 4123, 17 | endTime: 5555, 18 | text: 'Hello World 2' 19 | } 20 | ] 21 | 22 | var params = { 23 | Fontname: 'Verdana', 24 | Fontsize: '28', 25 | PrimaryColour: '11861244' 26 | } 27 | 28 | var lines = substation.stringify(subtitles, params).split('\n') 29 | lines.pop() 30 | 31 | expect(lines.shift()).to.be.equal('[Script Info]') 32 | expect(lines.shift()).to.be.equal('Title: Videoshow video') 33 | expect(lines.shift()).to.be.equal('ScriptType: v4') 34 | expect(lines.pop()).to.be.equal('Dialogue: Marked=0,0:00:04.12,0:00:05.55,DefaultVCD,NTP,0000,0000,0000,,Hello World 2') 35 | expect(lines.pop()).to.be.equal('Dialogue: Marked=0,0:00:01.00,0:00:03.00,DefaultVCD,NTP,0000,0000,0000,,Hello World') 36 | expect(lines.pop()).to.be.equal('Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text') 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/videoshow.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var rm = require('rimraf') 3 | var expect = require('chai').expect 4 | var videoshow = require('../') 5 | var TMP = 'test/.tmp' 6 | 7 | suite('videoshow', function () { 8 | var images = [ 9 | 'test/fixtures/step_1.png', 10 | 'test/fixtures/step_2.png', 11 | 'test/fixtures/step_3.png', 12 | 'test/fixtures/step_4.png', 13 | 'test/fixtures/step_5.png' 14 | ] 15 | 16 | before(function () { 17 | rm.sync(TMP) 18 | }) 19 | 20 | before(function () { 21 | fs.mkdirSync(TMP) 22 | }) 23 | 24 | test('create video with images', function (done) { 25 | videoshow(images) 26 | .save(TMP + '/test.mp4') 27 | .on('error', done) 28 | .on('end', function (output) { 29 | expect(fs.existsSync(output)).to.be.true 30 | done() 31 | }) 32 | }) 33 | 34 | test('create video with audio', function (done) { 35 | videoshow(images) 36 | .audio(__dirname + '/fixtures/song.aac') 37 | .save(TMP + '/test2.mp4') 38 | .on('error', done) 39 | .on('end', function (output) { 40 | expect(fs.existsSync(output)).to.be.true 41 | done() 42 | }) 43 | }) 44 | 45 | test('create video with subtitles', function (done) { 46 | videoshow(images) 47 | .audio(__dirname + '/fixtures/song.aac') 48 | .subtitles(__dirname + '/fixtures/subtitles.srt') 49 | .save(TMP + '/test3.mp4') 50 | .on('error', done) 51 | .on('end', function (output) { 52 | expect(fs.existsSync(output)).to.be.true 53 | done() 54 | }) 55 | }) 56 | 57 | test('create video with custom captions per images', function (done) { 58 | var imgs = images.map(function (path) { 59 | return { 60 | path: path, 61 | caption: 'This is a sample subtitle text', 62 | loop: 3 63 | } 64 | }) 65 | 66 | videoshow(imgs) 67 | .audio(__dirname + '/fixtures/song.aac') 68 | .subtitles(__dirname + '/fixtures/subtitles.srt') 69 | .save(TMP + '/test6.mp4') 70 | .on('error', done) 71 | .on('end', function (output) { 72 | expect(fs.existsSync(output)).to.be.true 73 | done() 74 | }) 75 | }) 76 | 77 | test('create video with mp3 audio', function (done) { 78 | videoshow(images) 79 | .audio(__dirname + '/fixtures/song.mp3') 80 | .save(TMP + '/test4.mp4') 81 | .on('error', done) 82 | .on('end', function (output) { 83 | expect(fs.existsSync(output)).to.be.true 84 | done() 85 | }) 86 | }) 87 | 88 | test('create video with ogg audio', function (done) { 89 | videoshow(images) 90 | .audio(__dirname + '/fixtures/song.ogg') 91 | .save(TMP + '/test5.mp4') 92 | .on('error', done) 93 | .on('end', function (output) { 94 | expect(fs.existsSync(output)).to.be.true 95 | done() 96 | }) 97 | }) 98 | }) 99 | --------------------------------------------------------------------------------