• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fluent-ffmpeg / node-fluent-ffmpeg / 9145903029

19 May 2024 07:14AM UTC coverage: 90.801%. Remained the same
9145903029

push

github

njoyard
Release v2.1.3

579 of 713 branches covered (81.21%)

1145 of 1261 relevant lines covered (90.8%)

339.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.7
/lib/processor.js
1
/*jshint node:true*/
2
'use strict';
3

4
var spawn = require('child_process').spawn;
6✔
5
var path = require('path');
6✔
6
var fs = require('fs');
6✔
7
var async = require('async');
6✔
8
var utils = require('./utils');
6✔
9

10
/*
11
 *! Processor methods
12
 */
13

14

15
/**
16
 * Run ffprobe asynchronously and store data in command
17
 *
18
 * @param {FfmpegCommand} command
19
 * @private
20
 */
21
function runFfprobe(command) {
22
  const inputProbeIndex = 0;
3✔
23
  if (command._inputs[inputProbeIndex].isStream) {
3!
24
    // Don't probe input streams as this will consume them
25
    return;
×
26
  }
27
  command.ffprobe(inputProbeIndex, function(err, data) {
3✔
28
    command._ffprobeData = data;
3✔
29
  });
30
}
31

32

33
module.exports = function(proto) {
6✔
34
  /**
35
   * Emitted just after ffmpeg has been spawned.
36
   *
37
   * @event FfmpegCommand#start
38
   * @param {String} command ffmpeg command line
39
   */
40

41
  /**
42
   * Emitted when ffmpeg reports progress information
43
   *
44
   * @event FfmpegCommand#progress
45
   * @param {Object} progress progress object
46
   * @param {Number} progress.frames number of frames transcoded
47
   * @param {Number} progress.currentFps current processing speed in frames per second
48
   * @param {Number} progress.currentKbps current output generation speed in kilobytes per second
49
   * @param {Number} progress.targetSize current output file size
50
   * @param {String} progress.timemark current video timemark
51
   * @param {Number} [progress.percent] processing progress (may not be available depending on input)
52
   */
53

54
  /**
55
   * Emitted when ffmpeg outputs to stderr
56
   *
57
   * @event FfmpegCommand#stderr
58
   * @param {String} line stderr output line
59
   */
60

61
  /**
62
   * Emitted when ffmpeg reports input codec data
63
   *
64
   * @event FfmpegCommand#codecData
65
   * @param {Object} codecData codec data object
66
   * @param {String} codecData.format input format name
67
   * @param {String} codecData.audio input audio codec name
68
   * @param {String} codecData.audio_details input audio codec parameters
69
   * @param {String} codecData.video input video codec name
70
   * @param {String} codecData.video_details input video codec parameters
71
   */
72

73
  /**
74
   * Emitted when an error happens when preparing or running a command
75
   *
76
   * @event FfmpegCommand#error
77
   * @param {Error} error error object, with optional properties 'inputStreamError' / 'outputStreamError' for errors on their respective streams
78
   * @param {String|null} stdout ffmpeg stdout, unless outputting to a stream
79
   * @param {String|null} stderr ffmpeg stderr
80
   */
81

82
  /**
83
   * Emitted when a command finishes processing
84
   *
85
   * @event FfmpegCommand#end
86
   * @param {Array|String|null} [filenames|stdout] generated filenames when taking screenshots, ffmpeg stdout when not outputting to a stream, null otherwise
87
   * @param {String|null} stderr ffmpeg stderr
88
   */
89

90

91
  /**
92
   * Spawn an ffmpeg process
93
   *
94
   * The 'options' argument may contain the following keys:
95
   * - 'niceness': specify process niceness, ignored on Windows (default: 0)
96
   * - `cwd`: change working directory
97
   * - 'captureStdout': capture stdout and pass it to 'endCB' as its 2nd argument (default: false)
98
   * - 'stdoutLines': override command limit (default: use command limit)
99
   *
100
   * The 'processCB' callback, if present, is called as soon as the process is created and
101
   * receives a nodejs ChildProcess object.  It may not be called at all if an error happens
102
   * before spawning the process.
103
   *
104
   * The 'endCB' callback is called either when an error occurs or when the ffmpeg process finishes.
105
   *
106
   * @method FfmpegCommand#_spawnFfmpeg
107
   * @param {Array} args ffmpeg command line argument list
108
   * @param {Object} [options] spawn options (see above)
109
   * @param {Function} [processCB] callback called with process object and stdout/stderr ring buffers when process has been created
110
   * @param {Function} endCB callback called with error (if applicable) and stdout/stderr ring buffers when process finished
111
   * @private
112
   */
113
  proto._spawnFfmpeg = function(args, options, processCB, endCB) {
6✔
114
    // Enable omitting options
115
    if (typeof options === 'function') {
144!
116
      endCB = processCB;
×
117
      processCB = options;
×
118
      options = {};
×
119
    }
120

121
    // Enable omitting processCB
122
    if (typeof endCB === 'undefined') {
144✔
123
      endCB = processCB;
18✔
124
      processCB = function() {};
18✔
125
    }
126

127
    var maxLines = 'stdoutLines' in options ? options.stdoutLines : this.options.stdoutLines;
144✔
128

129
    // Find ffmpeg
130
    this._getFfmpegPath(function(err, command) {
144✔
131
      if (err) {
144!
132
        return endCB(err);
×
133
      } else if (!command || command.length === 0) {
144!
134
        return endCB(new Error('Cannot find ffmpeg'));
×
135
      }
136

137
      // Apply niceness
138
      if (options.niceness && options.niceness !== 0 && !utils.isWindows) {
144!
139
        args.unshift('-n', options.niceness, command);
×
140
        command = 'nice';
×
141
      }
142

143
      var stdoutRing = utils.linesRing(maxLines);
144✔
144
      var stdoutClosed = false;
144✔
145

146
      var stderrRing = utils.linesRing(maxLines);
144✔
147
      var stderrClosed = false;
144✔
148

149
      // Spawn process
150
      var ffmpegProc = spawn(command, args, options);
144✔
151

152
      if (ffmpegProc.stderr) {
144!
153
        ffmpegProc.stderr.setEncoding('utf8');
144✔
154
      }
155

156
      ffmpegProc.on('error', function(err) {
144✔
157
        endCB(err);
×
158
      });
159

160
      // Ensure we wait for captured streams to end before calling endCB
161
      var exitError = null;
144✔
162
      function handleExit(err) {
163
        if (err) {
420✔
164
          exitError = err;
24✔
165
        }
166

167
        if (processExited && (stdoutClosed || !options.captureStdout) && stderrClosed) {
420✔
168
          endCB(exitError, stdoutRing, stderrRing);
144✔
169
        }
170
      }
171

172
      // Handle process exit
173
      var processExited = false;
144✔
174
      ffmpegProc.on('exit', function(code, signal) {
144✔
175
        processExited = true;
144✔
176

177
        if (signal) {
144✔
178
          handleExit(new Error('ffmpeg was killed with signal ' + signal));
12✔
179
        } else if (code) {
132✔
180
          handleExit(new Error('ffmpeg exited with code ' + code));
12✔
181
        } else {
182
          handleExit();
120✔
183
        }
184
      });
185

186
      // Capture stdout if specified
187
      if (options.captureStdout) {
144✔
188
        ffmpegProc.stdout.on('data', function(data) {
132✔
189
          stdoutRing.append(data);
80✔
190
        });
191

192
        ffmpegProc.stdout.on('close', function() {
132✔
193
          stdoutRing.close();
132✔
194
          stdoutClosed = true;
132✔
195
          handleExit();
132✔
196
        });
197
      }
198

199
      // Capture stderr if specified
200
      ffmpegProc.stderr.on('data', function(data) {
144✔
201
        stderrRing.append(data);
9,618✔
202
      });
203

204
      ffmpegProc.stderr.on('close', function() {
144✔
205
        stderrRing.close();
144✔
206
        stderrClosed = true;
144✔
207
        handleExit();
144✔
208
      });
209

210
      // Call process callback
211
      processCB(ffmpegProc, stdoutRing, stderrRing);
144✔
212
    });
213
  };
214

215

216
  /**
217
   * Build the argument list for an ffmpeg command
218
   *
219
   * @method FfmpegCommand#_getArguments
220
   * @return argument list
221
   * @private
222
   */
223
  proto._getArguments = function() {
6✔
224
    var complexFilters = this._complexFilters.get();
282✔
225

226
    var fileOutput = this._outputs.some(function(output) {
282✔
227
      return output.isFile;
282✔
228
    });
229

230
    return [].concat(
282✔
231
        // Inputs and input options
232
        this._inputs.reduce(function(args, input) {
233
          var source = (typeof input.source === 'string') ? input.source : 'pipe:0';
261✔
234

235
          // For each input, add input options, then '-i <source>'
236
          return args.concat(
261✔
237
            input.options.get(),
238
            ['-i', source]
239
          );
240
        }, []),
241

242
        // Global options
243
        this._global.get(),
244

245
        // Overwrite if we have file outputs
246
        fileOutput ? ['-y'] : [],
282✔
247

248
        // Complex filters
249
        complexFilters,
250

251
        // Outputs, filters and output options
252
        this._outputs.reduce(function(args, output) {
253
          var sizeFilters = utils.makeFilterStrings(output.sizeFilters.get());
339✔
254
          var audioFilters = output.audioFilters.get();
339✔
255
          var videoFilters = output.videoFilters.get().concat(sizeFilters);
339✔
256
          var outputArg;
257

258
          if (!output.target) {
339✔
259
            outputArg = [];
156✔
260
          } else if (typeof output.target === 'string') {
183✔
261
            outputArg = [output.target];
171✔
262
          } else {
263
            outputArg = ['pipe:1'];
12✔
264
          }
265

266
          return args.concat(
339✔
267
            output.audio.get(),
268
            audioFilters.length ? ['-filter:a', audioFilters.join(',')] : [],
339✔
269
            output.video.get(),
270
            videoFilters.length ? ['-filter:v', videoFilters.join(',')] : [],
339✔
271
            output.options.get(),
272
            outputArg
273
          );
274
        }, [])
275
      );
276
  };
277

278

279
  /**
280
   * Prepare execution of an ffmpeg command
281
   *
282
   * Checks prerequisites for the execution of the command (codec/format availability, flvtool...),
283
   * then builds the argument list for ffmpeg and pass them to 'callback'.
284
   *
285
   * @method FfmpegCommand#_prepare
286
   * @param {Function} callback callback with signature (err, args)
287
   * @param {Boolean} [readMetadata=false] read metadata before processing
288
   * @private
289
   */
290
  proto._prepare = function(callback, readMetadata) {
6✔
291
    var self = this;
129✔
292

293
    async.waterfall([
129✔
294
      // Check codecs and formats
295
      function(cb) {
296
        self._checkCapabilities(cb);
129✔
297
      },
298

299
      // Read metadata if required
300
      function(cb) {
301
        if (!readMetadata) {
126!
302
          return cb();
126✔
303
        }
304

305
        self.ffprobe(0, function(err, data) {
×
306
          if (!err) {
×
307
            self._ffprobeData = data;
×
308
          }
309

310
          cb();
×
311
        });
312
      },
313

314
      // Check for flvtool2/flvmeta if necessary
315
      function(cb) {
316
        var flvmeta = self._outputs.some(function(output) {
126✔
317
          // Remove flvmeta flag on non-file output
318
          if (output.flags.flvmeta && !output.isFile) {
183!
319
            self.logger.warn('Updating flv metadata is only supported for files');
×
320
            output.flags.flvmeta = false;
×
321
          }
322

323
          return output.flags.flvmeta;
183✔
324
        });
325

326
        if (flvmeta) {
126!
327
          self._getFlvtoolPath(function(err) {
×
328
            cb(err);
×
329
          });
330
        } else {
331
          cb();
126✔
332
        }
333
      },
334

335
      // Build argument list
336
      function(cb) {
337
        var args;
338
        try {
126✔
339
          args = self._getArguments();
126✔
340
        } catch(e) {
341
          return cb(e);
×
342
        }
343

344
        cb(null, args);
126✔
345
      },
346

347
      // Add "-strict experimental" option where needed
348
      function(args, cb) {
349
        self.availableEncoders(function(err, encoders) {
126✔
350
          for (var i = 0; i < args.length; i++) {
126✔
351
            if (args[i] === '-acodec' || args[i] === '-vcodec') {
2,214✔
352
              i++;
150✔
353

354
              if ((args[i] in encoders) && encoders[args[i]].experimental) {
150✔
355
                args.splice(i + 1, 0, '-strict', 'experimental');
3✔
356
                i += 2;
3✔
357
              }
358
            }
359
          }
360

361
          cb(null, args);
126✔
362
        });
363
      }
364
    ], callback);
365

366
    if (!readMetadata) {
129!
367
      // Read metadata as soon as 'progress' listeners are added
368

369
      if (this.listeners('progress').length > 0) {
129✔
370
        // Read metadata in parallel
371
        runFfprobe(this);
3✔
372
      } else {
373
        // Read metadata as soon as the first 'progress' listener is added
374
        this.once('newListener', function(event) {
126✔
375
          if (event === 'progress') {
×
376
            runFfprobe(this);
×
377
          }
378
        });
379
      }
380
    }
381
  };
382

383

384
  /**
385
   * Run ffmpeg command
386
   *
387
   * @method FfmpegCommand#run
388
   * @category Processing
389
   * @aliases exec,execute
390
   */
391
  proto.exec =
6✔
392
  proto.execute =
393
  proto.run = function() {
394
    var self = this;
129✔
395

396
    // Check if at least one output is present
397
    var outputPresent = this._outputs.some(function(output) {
129✔
398
      return 'target' in output;
129✔
399
    });
400

401
    if (!outputPresent) {
129!
402
      throw new Error('No output specified');
×
403
    }
404

405
    // Get output stream if any
406
    var outputStream = this._outputs.filter(function(output) {
129✔
407
      return typeof output.target !== 'string';
186✔
408
    })[0];
409

410
    // Get input stream if any
411
    var inputStream = this._inputs.filter(function(input) {
129✔
412
      return typeof input.source !== 'string';
138✔
413
    })[0];
414

415
    // Ensure we send 'end' or 'error' only once
416
    var ended = false;
129✔
417
    function emitEnd(err, stdout, stderr) {
418
      if (!ended) {
156✔
419
        ended = true;
129✔
420

421
        if (err) {
129✔
422
          self.emit('error', err, stdout, stderr);
27✔
423
        } else {
424
          self.emit('end', stdout, stderr);
102✔
425
        }
426
      }
427
    }
428

429
    self._prepare(function(err, args) {
129✔
430
      if (err) {
129✔
431
        return emitEnd(err);
3✔
432
      }
433

434
      // Run ffmpeg
435
      self._spawnFfmpeg(
126✔
436
        args,
437
        {
438
          captureStdout: !outputStream,
439
          niceness: self.options.niceness,
440
          cwd: self.options.cwd,
441
          windowsHide: true
442
        }, 
443

444
        function processCB(ffmpegProc, stdoutRing, stderrRing) {
445
          self.ffmpegProc = ffmpegProc;
126✔
446
          self.emit('start', 'ffmpeg ' + args.join(' '));
126✔
447

448
          // Pipe input stream if any
449
          if (inputStream) {
126✔
450
            inputStream.source.on('error', function(err) {
12✔
451
              var reportingErr = new Error('Input stream error: ' + err.message);
3✔
452
              reportingErr.inputStreamError = err;
3✔
453
              emitEnd(reportingErr);
3✔
454
              ffmpegProc.kill();
3✔
455
            });
456

457
            inputStream.source.resume();
12✔
458
            inputStream.source.pipe(ffmpegProc.stdin);
12✔
459

460
            // Set stdin error handler on ffmpeg (prevents nodejs catching the error, but
461
            // ffmpeg will fail anyway, so no need to actually handle anything)
462
            ffmpegProc.stdin.on('error', function() {});
12✔
463
          }
464

465
          // Setup timeout if requested
466
          if (self.options.timeout) {
126✔
467
            self.processTimer = setTimeout(function() {
15✔
468
              var msg = 'process ran into a timeout (' + self.options.timeout + 's)';
9✔
469

470
              emitEnd(new Error(msg), stdoutRing.get(), stderrRing.get());
9✔
471
              ffmpegProc.kill();
9✔
472
            }, self.options.timeout * 1000);
473
          }
474

475

476
          if (outputStream) {
126✔
477
            // Pipe ffmpeg stdout to output stream
478
            ffmpegProc.stdout.pipe(outputStream.target, outputStream.pipeopts);
12✔
479

480
            // Handle output stream events
481
            outputStream.target.on('close', function() {
12✔
482
              self.logger.debug('Output stream closed, scheduling kill for ffmpeg process');
12✔
483

484
              // Don't kill process yet, to give a chance to ffmpeg to
485
              // terminate successfully first  This is necessary because
486
              // under load, the process 'exit' event sometimes happens
487
              // after the output stream 'close' event.
488
              setTimeout(function() {
12✔
489
                emitEnd(new Error('Output stream closed'));
12✔
490
                ffmpegProc.kill();
12✔
491
              }, 20);
492
            });
493

494
            outputStream.target.on('error', function(err) {
12✔
495
              self.logger.debug('Output stream error, killing ffmpeg process');
3✔
496
              var reportingErr = new Error('Output stream error: ' + err.message);
3✔
497
              reportingErr.outputStreamError = err;
3✔
498
              emitEnd(reportingErr, stdoutRing.get(), stderrRing.get());
3✔
499
              ffmpegProc.kill('SIGKILL');
3✔
500
            });
501
          }
502

503
          // Setup stderr handling
504
          if (stderrRing) {
126!
505

506
            // 'stderr' event
507
            if (self.listeners('stderr').length) {
126✔
508
              stderrRing.callback(function(line) {
3✔
509
                self.emit('stderr', line);
102✔
510
              });
511
            }
512

513
            // 'codecData' event
514
            if (self.listeners('codecData').length) {
126✔
515
              var codecDataSent = false;
9✔
516
              var codecObject = {};
9✔
517

518
              stderrRing.callback(function(line) {
9✔
519
                if (!codecDataSent)
345✔
520
                  codecDataSent = utils.extractCodecData(self, line, codecObject);
168✔
521
              });
522
            }
523

524
            // 'progress' event
525
            if (self.listeners('progress').length) {
126✔
526
              stderrRing.callback(function(line) {
3✔
527
                utils.extractProgress(self, line);
123✔
528
              });
529
            }
530
          }
531
        },
532

533
        function endCB(err, stdoutRing, stderrRing) {
534
          clearTimeout(self.processTimer);
126✔
535
          delete self.ffmpegProc;
126✔
536

537
          if (err) {
126✔
538
            if (err.message.match(/ffmpeg exited with code/)) {
24✔
539
              // Add ffmpeg error message
540
              err.message += ': ' + utils.extractError(stderrRing.get());
12✔
541
            }
542

543
            emitEnd(err, stdoutRing.get(), stderrRing.get());
24✔
544
          } else {
545
            // Find out which outputs need flv metadata
546
            var flvmeta = self._outputs.filter(function(output) {
102✔
547
              return output.flags.flvmeta;
159✔
548
            });
549

550
            if (flvmeta.length) {
102!
551
              self._getFlvtoolPath(function(err, flvtool) {
×
552
                if (err) {
×
553
                  return emitEnd(err);
×
554
                }
555

556
                async.each(
×
557
                  flvmeta,
558
                  function(output, cb) {
559
                    spawn(flvtool, ['-U', output.target], {windowsHide: true})
×
560
                      .on('error', function(err) {
561
                        cb(new Error('Error running ' + flvtool + ' on ' + output.target + ': ' + err.message));
×
562
                      })
563
                      .on('exit', function(code, signal) {
564
                        if (code !== 0 || signal) {
×
565
                          cb(
×
566
                            new Error(flvtool + ' ' +
567
                              (signal ? 'received signal ' + signal
×
568
                                      : 'exited with code ' + code)) +
569
                              ' when running on ' + output.target
570
                          );
571
                        } else {
572
                          cb();
×
573
                        }
574
                      });
575
                  },
576
                  function(err) {
577
                    if (err) {
×
578
                      emitEnd(err);
×
579
                    } else {
580
                      emitEnd(null, stdoutRing.get(), stderrRing.get());
×
581
                    }
582
                  }
583
                );
584
              });
585
            } else {
586
              emitEnd(null, stdoutRing.get(), stderrRing.get());
102✔
587
            }
588
          }
589
        }
590
      );
591
    });
592

593
    return this;
129✔
594
  };
595

596

597
  /**
598
   * Renice current and/or future ffmpeg processes
599
   *
600
   * Ignored on Windows platforms.
601
   *
602
   * @method FfmpegCommand#renice
603
   * @category Processing
604
   *
605
   * @param {Number} [niceness=0] niceness value between -20 (highest priority) and 20 (lowest priority)
606
   * @return FfmpegCommand
607
   */
608
  proto.renice = function(niceness) {
6✔
609
    if (!utils.isWindows) {
6!
610
      niceness = niceness || 0;
6!
611

612
      if (niceness < -20 || niceness > 20) {
6✔
613
        this.logger.warn('Invalid niceness value: ' + niceness + ', must be between -20 and 20');
3✔
614
      }
615

616
      niceness = Math.min(20, Math.max(-20, niceness));
6✔
617
      this.options.niceness = niceness;
6✔
618

619
      if (this.ffmpegProc) {
6✔
620
        var logger = this.logger;
3✔
621
        var pid = this.ffmpegProc.pid;
3✔
622
        var renice = spawn('renice', [niceness, '-p', pid], {windowsHide: true});
3✔
623

624
        renice.on('error', function(err) {
3✔
625
          logger.warn('could not renice process ' + pid + ': ' + err.message);
×
626
        });
627

628
        renice.on('exit', function(code, signal) {
3✔
629
          if (signal) {
3!
630
            logger.warn('could not renice process ' + pid + ': renice was killed by signal ' + signal);
×
631
          } else if (code) {
3!
632
            logger.warn('could not renice process ' + pid + ': renice exited with ' + code);
×
633
          } else {
634
            logger.info('successfully reniced process ' + pid + ' to ' + niceness + ' niceness');
3✔
635
          }
636
        });
637
      }
638
    }
639

640
    return this;
6✔
641
  };
642

643

644
  /**
645
   * Kill current ffmpeg process, if any
646
   *
647
   * @method FfmpegCommand#kill
648
   * @category Processing
649
   *
650
   * @param {String} [signal=SIGKILL] signal name
651
   * @return FfmpegCommand
652
   */
653
  proto.kill = function(signal) {
6✔
654
    if (!this.ffmpegProc) {
12!
655
      this.logger.warn('No running ffmpeg process, cannot send signal');
×
656
    } else {
657
      this.ffmpegProc.kill(signal || 'SIGKILL');
12✔
658
    }
659

660
    return this;
12✔
661
  };
662
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc