diff --git a/flamenco_worker/commands.py b/flamenco_worker/commands.py index 4baf11ae4ac012986e71fc9e8565f636ba2989e4..4538ada7ad9977cda05c184e26b4878d759caf46 100644 --- a/flamenco_worker/commands.py +++ b/flamenco_worker/commands.py @@ -905,13 +905,16 @@ class CreateVideoCommand(AbstractSubprocessCommand): max_b_frames: typing.Optional[int] = 0 def validate(self, settings: Settings) -> typing.Optional[str]: - # Check that FFmpeg can be found. - ffmpeg, err = self._setting(settings, 'ffmpeg', is_required=False, default='ffmpeg') + # Check that FFmpeg can be found and shlex-split the string. + ffmpeg_cmd, err = self._setting(settings, 'ffmpeg_cmd', is_required=False, default='ffmpeg') if err: return err - executable_path: typing.Optional[str] = shutil.which(ffmpeg) + + cmd = shlex.split(ffmpeg_cmd) + executable_path: typing.Optional[str] = shutil.which(cmd[0]) if not executable_path: - return f'FFmpeg command {ffmpeg!r} not found on $PATH' + return f'FFmpeg command {ffmpeg_cmd!r} not found on $PATH' + settings['ffmpeg_cmd'] = cmd self._log.debug('Found FFmpeg command at %r', executable_path) # Check that we know our input and output image files. @@ -935,14 +938,17 @@ class CreateVideoCommand(AbstractSubprocessCommand): await self.subprocess(cmd) def _build_ffmpeg_command(self, settings) -> typing.List[str]: + assert isinstance(settings['ffmpeg_cmd'], list), \ + 'run validate() before _build_ffmpeg_command' cmd = [ - settings['ffmpeg'], + *settings['ffmpeg_cmd'], '-pattern_type', 'glob', '-i', settings['input_files'], '-c:v', self.codec_video, '-crf', str(self.constant_rate_factor), '-g', str(self.keyframe_interval), '-r', str(settings['fps']), + '-y', ] if self.max_b_frames is not None: cmd.extend(['-bf', str(self.max_b_frames)]) diff --git a/tests/test_commands_create_video.py b/tests/test_commands_create_video.py index 214f6253775ddcfa49c9366fe32d0d31bbe35635..6c554174b53e828d85fa517ebe3e90cdeef50936 100644 --- a/tests/test_commands_create_video.py +++ b/tests/test_commands_create_video.py @@ -14,7 +14,7 @@ log = logging.getLogger(__name__) class BlenderRenderTest(AbstractCommandTest): settings: typing.Dict[str, typing.Any] = { - 'ffmpeg': sys.executable, + 'ffmpeg_cmd': f'"{sys.executable}" -hide_banner', 'input_files': '/tmp/*.png', 'output_file': '/tmp/merged.mkv', 'fps': 24, @@ -30,31 +30,36 @@ class BlenderRenderTest(AbstractCommandTest): task_id='12345', command_idx=0, ) + self.settings = self.settings.copy() def test_validate(self): - self.assertIn('not found on $PATH', self.cmd.validate({'ffmpeg': '/does/not/exist'})) + self.assertIn('not found on $PATH', self.cmd.validate({'ffmpeg_cmd': '/does/not/exist'})) self.assertIsNone(self.cmd.validate(self.settings)) def test_validate_without_ffmpeg(self): settings = self.settings.copy() - del settings['ffmpeg'] + del settings['ffmpeg_cmd'] self.assertIsNone(self.cmd.validate(settings)) - self.assertEqual('ffmpeg', settings['ffmpeg'], + self.assertEqual(['ffmpeg'], settings['ffmpeg_cmd'], 'The default setting should be stored in the dict after validation') def test_build_ffmpeg_cmd(self): + self.cmd.validate(self.settings) + cliargs = self.cmd._build_ffmpeg_command(self.settings) + self.assertEqual([ - sys.executable, + sys.executable, '-hide_banner', '-pattern_type', 'glob', '-i', '/tmp/*.png', '-c:v', 'h264', '-crf', '17', '-g', '1', '-r', '24', + '-y', '-bf', '0', '/tmp/merged.mkv', - ], self.cmd._build_ffmpeg_command(self.settings)) + ], cliargs) def test_run_ffmpeg(self): with tempfile.TemporaryDirectory() as tempdir: @@ -62,7 +67,7 @@ class BlenderRenderTest(AbstractCommandTest): frame_dir = Path(__file__).with_name('test_frames') settings: typing.Dict[str, typing.Any] = { **self.settings, - 'ffmpeg': 'ffmpeg', # use the real FFmpeg for this test. + 'ffmpeg_cmd': 'ffmpeg', # use the real FFmpeg for this test. 'input_files': f'{frame_dir}/*.png', 'output_file': str(outfile), } @@ -77,5 +82,6 @@ class BlenderRenderTest(AbstractCommandTest): log.debug('Running %s', ' '.join(shlex.quote(arg) for arg in ffprobe_cmd)) probe_out = subprocess.check_output(ffprobe_cmd) probed_duration = float(probe_out) - expect_duration = len(list(frame_dir.glob('*.png'))) / settings['fps'] + fps: int = settings['fps'] + expect_duration = len(list(frame_dir.glob('*.png'))) / fps self.assertAlmostEqual(expect_duration, probed_duration, places=3)