diff --git a/lib/ffmpeg.rb b/lib/ffmpeg.rb index f5d8850..d186f5c 100644 --- a/lib/ffmpeg.rb +++ b/lib/ffmpeg.rb @@ -113,10 +113,12 @@ def ffmpeg_binary # Safely captures the standard output and the standard error of the ffmpeg command. # # @param args [Array] The arguments to pass to ffmpeg. + # @param spawn [Hash] Options hash forwarded to Process.spawn # @return [Array] The standard output, the standard error, and the process status. - def ffmpeg_capture3(*args) + def ffmpeg_capture3(*args, spawn: nil) logger.debug(self) { "ffmpeg -y #{Shellwords.join(args)}" } - FFMPEG::IO.capture3(ffmpeg_binary, '-y', *args) + spawn ||= {} + FFMPEG::IO.capture3(ffmpeg_binary, '-y', *args, **spawn) end # Starts a new ffmpeg process with the given arguments. @@ -125,29 +127,33 @@ def ffmpeg_capture3(*args) # to the specified block. # # @param args [Array] The arguments to pass to ffmpeg. + # @param spawn [Hash] Options hash forwarded to Process.spawn # @yieldparam stdin (+IO+) The standard input stream. # @yieldparam stdout (+FFMPEG::IO+) The standard output stream. # @yieldparam stderr (+FFMPEG::IO+) The standard error stream. # @yieldparam wait_thr (+Thread+) The child process thread. # @return [Process::Status, Array] - def ffmpeg_popen3(*args, &) + def ffmpeg_popen3(*args, spawn: nil, &) logger.debug(self) { "ffmpeg -y #{Shellwords.join(args)}" } - FFMPEG::IO.popen3(ffmpeg_binary, '-y', *args, &) + spawn ||= {} + FFMPEG::IO.popen3(ffmpeg_binary, '-y', *args, **spawn, &) end # Execute a ffmpeg command. # # @param args [Array] The arguments to pass to ffmpeg. # @param reporters [Array>] The reporters to use to parse the output. + # @param spawn [Hash] Options hash forwarded to Process.spawn # @yield [report] Reports from the ffmpeg command (see FFMPEG::Reporters). # @return [FFMPEG::Status] - def ffmpeg_execute(*args, status: nil, reporters: nil, timeout: nil) + def ffmpeg_execute(*args, status: nil, reporters: nil, timeout: nil, spawn: nil) status ||= FFMPEG::Status.new reporters ||= self.reporters timeout ||= self.timeout status.bind! do - ffmpeg_popen3(*args) do |_stdin, stdout, stderr, wait_thr| + spawn ||= {} + ffmpeg_popen3(*args, spawn:) do |_stdin, stdout, stderr, wait_thr| Timeout.timeout(timeout) do stderr.each(chomp: true) do |line| reporter = reporters.find { |r| r.match?(line) } @@ -171,10 +177,11 @@ def ffmpeg_execute(*args, status: nil, reporters: nil, timeout: nil) # # @param args [Array] The arguments to pass to ffmpeg. # @param reporters [Array] The reporters to use to parse the output. + # @param spawn [Hash] Options hash forwarded to Process.spawn # @yield [report] Reports from the ffmpeg command (see FFMPEG::Reporters). # @return [FFMPEG::Status] - def ffmpeg_execute!(*args, status: nil, reporters: nil, timeout: nil) - ffmpeg_execute(*args, status:, reporters:, timeout:).assert! + def ffmpeg_execute!(*args, status: nil, reporters: nil, timeout: nil, spawn: nil) + ffmpeg_execute(*args, status:, reporters:, timeout:, spawn:).assert! end # Get the path to the ffprobe binary. @@ -205,9 +212,10 @@ def ffprobe_binary=(path) # @param args [Array] The arguments to pass to ffprobe. # @return [Array] The standard output, the standard error, and the process status. # @raise [Errno::ENOENT] If the ffprobe binary cannot be found. - def ffprobe_capture3(*args) + def ffprobe_capture3(*args, spawn: nil) logger.debug(self) { "ffprobe -y #{Shellwords.join(args)}" } - FFMPEG::IO.capture3(ffprobe_binary, '-y', *args) + spawn ||= {} + FFMPEG::IO.capture3(ffprobe_binary, '-y', *args, **spawn) end # Starts a new ffprobe process with the given arguments. @@ -216,14 +224,16 @@ def ffprobe_capture3(*args) # to the specified block. # # @param args [Array] The arguments to pass to ffprobe. + # @param spawn [Hash] Options hash forwarded to Process.spawn # @yieldparam stdin (+IO+) The standard input stream. # @yieldparam stdout (+FFMPEG::IO+) The standard output stream. # @yieldparam stderr (+FFMPEG::IO+) The standard error stream. # @return [Process::Status, Array] # @raise [Errno::ENOENT] If the ffprobe binary cannot be found. - def ffprobe_popen3(*args, &) + def ffprobe_popen3(*args, spawn: nil, &) logger.debug(self) { "ffprobe -y #{Shellwords.join(args)}" } - FFMPEG::IO.popen3(ffprobe_binary, '-y', *args, &) + spawn ||= {} + FFMPEG::IO.popen3(ffprobe_binary, '-y', *args, **spawn, &) end # Cross-platform way of finding an executable in the $PATH. diff --git a/lib/ffmpeg/io.rb b/lib/ffmpeg/io.rb index a7c595c..e5268c9 100644 --- a/lib/ffmpeg/io.rb +++ b/lib/ffmpeg/io.rb @@ -30,15 +30,15 @@ def extend!(io) io.extend(FFMPEG::IO) end - def capture3(*cmd) - *io, status = Open3.capture3(*cmd) + def capture3(*cmd, **spawn_opts) + *io, status = Open3.capture3(*cmd, **spawn_opts) io.each(&method(:encode!)) [*io, status] end - def popen3(*cmd, &block) + def popen3(*cmd, **spawn_opts, &block) if block_given? - Open3.popen3(*cmd) do |*io, wait_thr| + Open3.popen3(*cmd, **spawn_opts) do |*io, wait_thr| io = io.map(&method(:extend!)) block.call(*io, wait_thr) rescue StandardError @@ -47,7 +47,7 @@ def popen3(*cmd, &block) raise end else - *io, wait_thr = Open3.popen3(*cmd) + *io, wait_thr = Open3.popen3(*cmd, **spawn_opts) io = io.map(&method(:extend!)) [*io, wait_thr] end diff --git a/lib/ffmpeg/media.rb b/lib/ffmpeg/media.rb index 05eba18..16a1177 100644 --- a/lib/ffmpeg/media.rb +++ b/lib/ffmpeg/media.rb @@ -530,8 +530,10 @@ def local? # @param inargs [Array] The arguments to pass before the input. # @yield [report] Reports from the ffmpeg command (see FFMPEG::Reporters). # @return [Process::Status] - def ffmpeg_execute(*args, inargs: [], status: nil, reporters: nil, timeout: nil, &block) - FFMPEG.ffmpeg_execute(*inargs, '-i', path, *args, status:, reporters:, timeout:, &block) + def ffmpeg_execute(*args, inargs: [], status: nil, reporters: nil, timeout: nil, + spawn: nil, &block) + FFMPEG.ffmpeg_execute(*inargs, '-i', path, *args, status:, reporters:, timeout:, + spawn:, &block) end # Execute a ffmpeg command with the media as input @@ -541,8 +543,9 @@ def ffmpeg_execute(*args, inargs: [], status: nil, reporters: nil, timeout: nil, # @param inargs [Array] The arguments to pass before the input. # @yield [report] Reports from the ffmpeg command (see FFMPEG::Reporters). # @return [Process::Status] - def ffmpeg_execute!(*args, inargs: [], status: nil, reporters: nil, timeout: nil, &block) - ffmpeg_execute(*args, inargs:, status:, reporters:, timeout:, &block).assert! + def ffmpeg_execute!(*args, inargs: [], status: nil, reporters: nil, timeout: nil, + spawn: nil, &block) + ffmpeg_execute(*args, inargs:, status:, reporters:, timeout:, spawn:, &block).assert! end end end diff --git a/lib/ffmpeg/transcoder.rb b/lib/ffmpeg/transcoder.rb index 18eb880..35419a6 100644 --- a/lib/ffmpeg/transcoder.rb +++ b/lib/ffmpeg/transcoder.rb @@ -98,9 +98,10 @@ def initialize( # # @param media [String, Pathname, URI, FFMPEG::Media] The media file to transcode. # @param output_path [String, Pathname] The output path to save the transcoded files. + # @param chdir [String, Pathname] Change the ffmpeg working directory # @yield The block to execute to report the transcoding process. # @return [FFMPEG::Transcoder::Status] The status of the transcoding process. - def process(media, output_path, &) + def process(media, output_path, chdir: nil, &) status = nil attempts = 0 @@ -126,11 +127,13 @@ def process(media, output_path, &) end inargs = CommandArgs.compose(media, context:, &@compose_inargs).to_a + spawn = chdir ? { chdir: } : {} status = media.ffmpeg_execute( *args, inargs:, reporters:, timeout:, + spawn:, status: Status.new(output_paths, checks:), & ) @@ -148,10 +151,11 @@ def process(media, output_path, &) # # @param media [String, Pathname, URI, FFMPEG::Media] The media file to transcode. # @param output_path [String, Pathname] The output path to save the transcoded files. + # @param chdir [String, Pathname] Change the ffmpeg working directory # @yield The block to execute to report the transcoding process. # @return [FFMPEG::Transcoder::Status] The status of the transcoding process. - def process!(media, output_path, &) - process(media, output_path, &).assert! + def process!(media, output_path, chdir: nil, &) + process(media, output_path, chdir:, &).assert! end end end diff --git a/spec/ffmpeg/media_spec.rb b/spec/ffmpeg/media_spec.rb index 14d21af..bd2122c 100644 --- a/spec/ffmpeg/media_spec.rb +++ b/spec/ffmpeg/media_spec.rb @@ -625,7 +625,7 @@ module FFMPEG block = proc {} expect(status).to receive(:assert!).and_return(status) - expect(subject).to receive(:ffmpeg_execute).with(*args, inargs:, status:, reporters:, timeout:, + expect(subject).to receive(:ffmpeg_execute).with(*args, inargs:, status:, reporters:, timeout:, spawn: nil, &block).and_return(status) expect(subject.ffmpeg_execute!(*args, inargs:, status:, reporters:, timeout:, &block)).to be(status) end diff --git a/spec/ffmpeg/transcoder_spec.rb b/spec/ffmpeg/transcoder_spec.rb index 77ec74f..12e2f7a 100644 --- a/spec/ffmpeg/transcoder_spec.rb +++ b/spec/ffmpeg/transcoder_spec.rb @@ -117,6 +117,17 @@ module FFMPEG expect(attempts).to eq(2) end end + + it 'can run ffmpeg in a different working directory (chdir)' do + output_path = File.join(tmp_dir, SecureRandom.hex(4)) + status = double(Transcoder::Status, success?: true) + + expect(FFMPEG::IO).to receive(:popen3) + .with(any_args, chdir: tmp_dir) + .and_return(status) + + subject.process(media, output_path, chdir: tmp_dir) + end end describe '#process!' do @@ -127,7 +138,9 @@ module FFMPEG block = proc {} expect(status).to receive(:assert!).and_return(status) - expect(subject).to receive(:process).with(media, output_path, &block).and_return(status) + expect(subject).to receive(:process) + .with(media, output_path, chdir: nil, &block) + .and_return(status) expect(subject.process!(media, output_path, &block)).to eq(status) end end diff --git a/spec/ffmpeg_spec.rb b/spec/ffmpeg_spec.rb index f0b25da..5679b08 100644 --- a/spec/ffmpeg_spec.rb +++ b/spec/ffmpeg_spec.rb @@ -61,6 +61,13 @@ expect(status.exitstatus).to eq(0) end + it 'forwards spawn parameter to popen3' do + spawn = { chdir: '/tmp/test' } + + expect(FFMPEG::IO).to receive(:popen3).with(any_args, chdir: '/tmp/test') + described_class.ffmpeg_execute(spawn:) + end + context 'when ffmpeg hangs' do before do described_class.ffmpeg_binary = fixture_file('bin/mock-ffmpeg')