Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
13 changes: 1 addition & 12 deletions lib/ffmpeg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand All @@ -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
Expand Down Expand Up @@ -145,7 +134,7 @@ def ffmpeg_popen3(*args, &)
# Execute a ffmpeg command.
#
# @param args [Array<String>] The arguments to pass to ffmpeg.
# @param reporters [Array<FFMPEG::Reporters::Output>] The reporters to use to parse the output.
# @param reporters [Array<Class<FFMPEG::Reporters::Output>>] 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)
Expand Down
30 changes: 18 additions & 12 deletions lib/ffmpeg/io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion lib/ffmpeg/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module FFMPEG
VERSION = '7.0.0-beta.12'
VERSION = '7.0.0-beta.13'
end
63 changes: 43 additions & 20 deletions spec/ffmpeg_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -89,21 +76,57 @@
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
end

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)
Comment thread
bajankristof marked this conversation as resolved.
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

Expand Down
30 changes: 21 additions & 9 deletions spec/fixtures/bin/ffmpeg-hanging → spec/fixtures/bin/mock-ffmpeg
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
Comment thread
bajankristof marked this conversation as resolved.
$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
1 change: 0 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
require 'bundler'
Bundler.require

require 'debug'
require 'fileutils'
require 'uri'
require 'webmock/rspec'
Expand Down