diff --git a/CHANGELOG b/CHANGELOG index d132eb3..8a54179 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +== 7.0.0-beta.13 2025-04-23 + +Fixes: +* Fixed a bug that caused the `FFMPEG::IO#each` method to crash when the parent process + was receiving and trapping exit signals. + == 7.0.0-beta.12 2025-04-15 Breaking Changes: diff --git a/lib/ffmpeg.rb b/lib/ffmpeg.rb index ad48210..ae125e4 100644 --- a/lib/ffmpeg.rb +++ b/lib/ffmpeg.rb @@ -32,15 +32,6 @@ require_relative 'ffmpeg/transcoder' require_relative 'ffmpeg/version' -if RUBY_PLATFORM =~ /(win|w)(32|64)$/ - begin - require 'win32/process' - rescue LoadError - 'Warning: ffmpeg is missing the win32-process gem to properly handle hanging transcodings. ' \ - 'Install the gem (in Gemfile if using bundler) to avoid errors.' - end -end - # The FFMPEG module allows you to customise the behaviour of the FFMPEG library, # and provides a set of methods to directly interact with the ffmpeg and ffprobe binaries. # @@ -51,8 +42,6 @@ # FFMPEG.ffmpeg_binary = '/usr/local/bin/ffmpeg' # FFMPEG.ffprobe_binary = '/usr/local/bin/ffprobe' module FFMPEG - SIGKILL = RUBY_PLATFORM =~ /(win|w)(32|64)$/ ? 1 : 'SIGKILL' - class << self attr_writer :logger, :reporters attr_accessor :threads, :timeout @@ -145,7 +134,7 @@ def ffmpeg_popen3(*args, &) # 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 reporters [Array>] The reporters to use to parse the output. # @yield [report] Reports from the ffmpeg command (see FFMPEG::Reporters). # @return [FFMPEG::Status] def ffmpeg_execute(*args, status: nil, reporters: nil, timeout: nil) diff --git a/lib/ffmpeg/io.rb b/lib/ffmpeg/io.rb index e20efa0..a7c595c 100644 --- a/lib/ffmpeg/io.rb +++ b/lib/ffmpeg/io.rb @@ -55,21 +55,27 @@ def popen3(*cmd, &block) end def each(chomp: false, &block) - buffer = String.new + # We need to run this loop in a separate thread to avoid + # errors with exit signals being sent to the main thread. + Thread.new do + Thread.current.report_on_exception = false - until eof? - char = getc - case char - when "\r", "\n" - buffer << ($ORS || "\n") unless chomp - block.call(buffer) unless buffer.empty? - buffer = String.new - else - buffer << FFMPEG::IO.encode!(char) + buffer = String.new + + until eof? + char = getc + case char + when "\r", "\n" + buffer << ($ORS || "\n") unless chomp + block.call(buffer) unless buffer.empty? + buffer.clear + else + buffer << FFMPEG::IO.encode!(char) + end end - end - block.call(buffer) unless buffer.empty? + block.call(buffer) unless buffer.empty? + end.value end end end diff --git a/lib/ffmpeg/version.rb b/lib/ffmpeg/version.rb index ce82eac..474234e 100644 --- a/lib/ffmpeg/version.rb +++ b/lib/ffmpeg/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module FFMPEG - VERSION = '7.0.0-beta.12' + VERSION = '7.0.0-beta.13' end diff --git a/spec/ffmpeg_spec.rb b/spec/ffmpeg_spec.rb index 5d5682f..f0b25da 100644 --- a/spec/ffmpeg_spec.rb +++ b/spec/ffmpeg_spec.rb @@ -53,30 +53,17 @@ end describe '.ffmpeg_execute' do - let(:args) { ['-i', fixture_media_file('hello.wav'), '-f', 'null', '/dev/null'] } - - it 'returns the process status and yields reports' do - reports = [] - - status = described_class.ffmpeg_execute( - *args, - reporters: [FFMPEG::Reporters::Output] - ) do |report| - reports << report - end + it 'returns the process status' do + args = ['-i', fixture_media_file('hello.wav'), '-f', 'null', '-'] + status = described_class.ffmpeg_execute(*args) expect(status).to be_a(FFMPEG::Status) expect(status.exitstatus).to eq(0) - expect(reports.length).to be >= 1 end context 'when ffmpeg hangs' do before do - FFMPEG.ffmpeg_binary = fixture_file('bin/ffmpeg-hanging') - end - - after do - FFMPEG.ffmpeg_binary = nil + described_class.ffmpeg_binary = fixture_file('bin/mock-ffmpeg') end context 'with IO timeout set' do @@ -89,13 +76,13 @@ end it 'raises IO::TimeoutError' do - expect { described_class.ffmpeg_execute(*args) }.to raise_error(IO::TimeoutError) + expect { described_class.ffmpeg_execute!('hello', 'world') }.to raise_error(IO::TimeoutError) end end context 'with operation timeout set' do it 'raises Timeout::Error' do - expect { described_class.ffmpeg_execute(*args, timeout: 0.5) }.to raise_error(Timeout::Error) + expect { described_class.ffmpeg_execute!('hello', 'world', timeout: 0.5) }.to raise_error(Timeout::Error) end end end @@ -103,7 +90,43 @@ describe '.ffmpeg_execute!' do it 'raises an error when the process is unsuccessful' do - expect { FFMPEG.ffmpeg_execute!('-v') }.to raise_error(FFMPEG::Error) + expect { described_class.ffmpeg_execute!('-v') }.to raise_error(FFMPEG::Error) + end + + context 'when called in a subprocess' do + before do + described_class.ffmpeg_binary = fixture_file('bin/mock-ffmpeg') + end + + context 'with exit signal traps' do + it 'does not raise an error' do + pid = fork do + Signal.trap('QUIT') {} # rubocop:disable Lint/EmptyBlock + + described_class.ffmpeg_execute!('-n=3', '-progress', 'hello', 'world') + end + + sleep 1 + Process.kill('QUIT', pid) + Process.wait(pid) + + expect($CHILD_STATUS&.exitstatus).to eq(0) + end + end + + context 'without exit signal traps' do + it 'does not raise an error' do + pid = fork do + described_class.ffmpeg_execute!('-n=10', '-progress', 'hello', 'world') + end + + sleep 1 + Process.kill('QUIT', pid) + _, status = Process.wait2(pid) + + expect(status.exitstatus).not_to eq(0) + end + end end end diff --git a/spec/fixtures/bin/ffmpeg-hanging b/spec/fixtures/bin/mock-ffmpeg similarity index 82% rename from spec/fixtures/bin/ffmpeg-hanging rename to spec/fixtures/bin/mock-ffmpeg index 8695ba7..4fa1ef9 100755 --- a/spec/fixtures/bin/ffmpeg-hanging +++ b/spec/fixtures/bin/mock-ffmpeg @@ -1,6 +1,18 @@ #!/usr/bin/env ruby # frozen_string_literal: true +def iterations + return @iterations if defined?(@iterations) + + @iterations ||= ARGV.find { |arg| arg =~ /-n/ }&.split('=')&.last&.to_i +end + +def progress? + return @progress if defined?(@progress) + + @progress ||= ARGV.include?('-progress') +end + warn <<~OUTPUT ffmpeg version 0.11.1 Copyright (c) 2000-2012 the FFmpeg developers built on Jun 27 2012 11:39:49 with llvm_gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00) @@ -29,17 +41,17 @@ warn <<~OUTPUT Metadata: creation_time : 1970-01-01 00:00:00 handler_name : SoundHandler -OUTPUT - -if ARGV.length > 2 # looks like we're trying to transcode - warn <<-OUTPUT Stream mapping: Stream #0:0 -> #0:0 (h264 -> libx264) Stream #0:1 -> #0:1 (aac -> libfaac) Press [q] to stop, [?] for help - OUTPUT - $stderr.write 'frame= 72 fps=0.0 q=32766.0 Lsize= 115kB time=00:00:07.00 bitrate= 134.6kbits/s' - loop { sleep 1 } -else - warn 'At least one output file must be specified' +OUTPUT + +n = 0 +loop do + $stderr.write 'frame=0 fps=0.0 q=0.0 Lsize=0kB time=00:00:00.00 bitrate=0.0kbits/s' if progress? + + break if iterations && (n += 1) > iterations + + sleep 1 end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0c6efb0..19e56fe 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,7 +6,6 @@ require 'bundler' Bundler.require -require 'debug' require 'fileutils' require 'uri' require 'webmock/rspec'