diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9e1305e0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [master, relatel] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby-version: ['3.3', '3.4', '4.0', 'head'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install ffmpeg + run: sudo apt-get update && sudo apt-get install -y ffmpeg + + - name: Set up Ruby ${{ matrix.ruby-version }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + + - name: Run tests + run: bundle exec rake diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..90cdbdcb --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-4.0.1 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8d7b70b7..00000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -before_install: wget http://johnvansickle.com/ffmpeg/releases/ffmpeg-release-64bit-static.tar.xz -O - | sudo tar -xJ --strip-components=1 -C /usr/local/bin/ -language: ruby -rvm: - - 2.0.0 - - 2.2 - - 2.3.1 -cache: - - bundler -addons: - code_climate: - repo_token: 25cb5ce0278a4633050937ff504ef0eef45f5756b53db51b179ad42ed5d8c428 -after_success: - - bundle exec codeclimate-test-reporter diff --git a/CHANGELOG b/CHANGELOG index cf74b064..d321e40c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,16 @@ == Master -Current with 3.0.2 +Breaking Changes: +* Require Ruby >= 3.3 +* Removed multi_json dependency — uses stdlib JSON +* Removed IO monkey patch — timeout handling is now private to Transcoder +* transcode/screenshot now accept keyword arguments for transcoder_options + +Improvements: +* GitHub Actions CI replacing Travis +* Fixed rotation detection for modern ffmpeg (side_data_list) +* Fixed SSL detection (scheme instead of port) +* General Ruby 3.x/4.0 compatibility fixes == 3.0.2 2016-11-18 diff --git a/Gemfile b/Gemfile index 72aeda35..68db28f0 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" gemspec -group :test do - gem 'webmock' - gem 'simplecov' -end \ No newline at end of file +gem 'webrick' +gem 'webmock' +gem 'rspec' +gem 'rake' diff --git a/README.md b/README.md index dd2d83cc..12855497 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,10 @@ Streamio FFMPEG =============== -[![Build Status](https://travis-ci.org/bikeath1337/streamio-ffmpeg.svg?branch=master)](https://travis-ci.org/bikeath1337/streamio-ffmpeg) -[![Code Climate](https://codeclimate.com/github/bikeath1337/streamio-ffmpeg/badges/gpa.svg)](https://codeclimate.com/github/bikeath1337/streamio-ffmpeg) -[![Test Coverage](https://codeclimate.com/github/bikeath1337/streamio-ffmpeg/badges/coverage.svg)](https://codeclimate.com/github/bikeath1337/streamio-ffmpeg/coverage) - Simple yet powerful wrapper around the ffmpeg command for reading metadata and transcoding movies. All work on this project is sponsored by the online video platform [Streamio](https://streamio.com) from [Rackfish](https://www.rackfish.com). -[![Streamio](http://d253c4ja9jigvu.cloudfront.net/assets/small-logo.png)](https://streamio.com) - Installation ------------ @@ -21,17 +15,11 @@ Compatibility ### Ruby -Only guaranteed to work with MRI Ruby 1.9.3 or later. -Should work with rubinius head in 1.9 mode. -Will not work in jruby until they fix: http://goo.gl/Z4UcX (should work in the upcoming 1.7.5) +Requires Ruby 3.3 or later. ### ffmpeg -The current gem is tested against ffmpeg 2.8.4. So no guarantees with earlier (or much later) -versions. Output and input standards have inconveniently changed rather a lot between versions -of ffmpeg. My goal is to keep this library in sync with new versions of ffmpeg as they come along. - -On macOS: `brew install ffmpeg`. +Should work with any modern ffmpeg. Install with your package manager, e.g. on macOS: `brew install ffmpeg`. Usage ----- @@ -127,11 +115,9 @@ widescreen_movie = FFMPEG::Movie.new("path/to/widescreen_movie.mov") options = { resolution: "320x240" } -transcoder_options = { preserve_aspect_ratio: :width } -widescreen_movie.transcode("movie.mp4", options, transcoder_options) # Output resolution will be 320x180 +widescreen_movie.transcode("movie.mp4", options, preserve_aspect_ratio: :width) # Output resolution will be 320x180 -transcoder_options = { preserve_aspect_ratio: :height } -widescreen_movie.transcode("movie.mp4", options, transcoder_options) # Output resolution will be 426x240 +widescreen_movie.transcode("movie.mp4", options, preserve_aspect_ratio: :height) # Output resolution will be 426x240 ``` For constant bitrate encoding use video_min_bitrate and video_max_bitrate with buffer_size. @@ -143,14 +129,12 @@ movie.transcode("movie.flv", options) ### Specifying Input Options -To specify which options apply the input, such as changing the input framerate, use `input_options` hash -in the transcoder_options. +To specify which options apply the input, such as changing the input framerate, use `input_options` keyword argument. ``` ruby movie = FFMPEG::Movie.new("path/to/movie.mov") -transcoder_options = { input_options: { framerate: '1/5' } } -movie.transcode("movie.mp4", {}, transcoder_options) +movie.transcode("movie.mp4", {}, input_options: { framerate: '1/5' }) # FFMPEG Command will look like this: # ffmpeg -y -framerate 1/5 -i path/to/movie.mov movie.mp4 @@ -158,14 +142,13 @@ movie.transcode("movie.mp4", {}, transcoder_options) ### Overriding the Input Path -If FFMPEG's input path needs to specify a sequence of files, rather than a path to a single movie, transcoding_options -`input` can be set. If this option is present, the path of the original movie will not be used. +If FFMPEG's input path needs to specify a sequence of files, rather than a path to a single movie, the +`input` keyword argument can be set. If this option is present, the path of the original movie will not be used. ``` ruby movie = FFMPEG::Movie.new("path/to/movie.mov") -transcoder_options = { input: 'img_%03d.png' } -movie.transcode("movie.mp4", {}, transcoder_options) +movie.transcode("movie.mp4", {}, input: 'img_%03d.png') # FFMPEG Command will look like this: # ffmpeg -y -i img_%03d.png movie.mp4 @@ -244,7 +227,7 @@ slideshow = slideshow_transcoder.run Specify the path to ffmpeg -------------------------- -By default, the gem assumes that the ffmpeg binary is available in the execution path and named ffmpeg and so will run commands that look something like `ffmpeg -i /path/to/input.file ...`. Use the FFMPEG.ffmpeg_binary setter to specify the full path to the binary if necessary: +By default, the gem finds the ffmpeg binary from your PATH. Use the FFMPEG.ffmpeg_binary setter to specify the full path to the binary if necessary: ``` ruby FFMPEG.ffmpeg_binary = '/usr/local/bin/ffmpeg' @@ -272,14 +255,13 @@ Disabling output file validation By default Transcoder validates the output file, in case you use FFMPEG for HLS format that creates multiple outputs you can disable the validation by passing -`validate: false` to transcoder_options. +`validate: false`. Note that transcode will not return the encoded movie object in this case since attempting to open a (possibly) invalid output file might result in an error being raised. ```ruby -transcoder_options = { validate: false } -movie.transcode("movie.mp4", options, transcoder_options) # returns nil +movie.transcode("movie.mp4", options, validate: false) # returns nil ``` Copyright diff --git a/Rakefile b/Rakefile index 59831c9d..08ed6978 100644 --- a/Rakefile +++ b/Rakefile @@ -7,7 +7,7 @@ RSpec::Core::RakeTask.new('spec') do |t| t.pattern = FileList['spec/**/*_spec.rb'] end -task :default => :spec +task default: :spec desc "Push a new version to Rubygems" task :publish do diff --git a/lib/ffmpeg/encoding_options.rb b/lib/ffmpeg/encoding_options.rb index 4bbf2369..5ad19a37 100644 --- a/lib/ffmpeg/encoding_options.rb +++ b/lib/ffmpeg/encoding_options.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module FFMPEG class EncodingOptions < Hash def initialize(options = {}) @@ -26,7 +28,7 @@ def to_a keys.sort_by{|k| params_order(k) }.each do |key| value = self[key] - a = send("convert_#{key}", value) if value && supports_option?(key) + a = send(:"convert_#{key}", value) if value && supports_option?(key) params += a unless a.nil? end @@ -44,8 +46,7 @@ def height private def supports_option?(option) - option = RUBY_VERSION < "1.9" ? "convert_#{option}" : "convert_#{option}".to_sym - private_methods.include?(option) + respond_to?(:"convert_#{option}", true) end def convert_aspect(value) diff --git a/lib/ffmpeg/errors.rb b/lib/ffmpeg/errors.rb index ac862d7d..37ff5cb3 100644 --- a/lib/ffmpeg/errors.rb +++ b/lib/ffmpeg/errors.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module FFMPEG class Error < StandardError end diff --git a/lib/ffmpeg/io_monkey.rb b/lib/ffmpeg/io_monkey.rb deleted file mode 100644 index 15fa8bbc..00000000 --- a/lib/ffmpeg/io_monkey.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'timeout' -require 'thread' -if RUBY_PLATFORM =~ /(win|w)(32|64)$/ - begin - require 'win32/process' - rescue LoadError - "Warning: streamio-ffmpeg is missing the win32-process gem to properly handle hung transcodings. Install the gem (in Gemfile if using bundler) to avoid errors." - end -end - -# -# Monkey Patch timeout support into the IO class -# -class IO - def each_with_timeout(pid, seconds, sep_string=$/) - last_update = Time.now - - current_thread = Thread.current - check_update_thread = Thread.new do - loop do - sleep 0.1 - if last_update - Time.now < -seconds - current_thread.raise Timeout::Error.new('output wait time expired') - end - end - end - - each(sep_string) do |buffer| - last_update = Time.now - yield buffer - end - rescue Timeout::Error - if RUBY_PLATFORM =~ /(win|w)(32|64)$/ - Process.kill(1, pid) - else - Process.kill('SIGKILL', pid) - end - raise - ensure - check_update_thread.kill - end -end diff --git a/lib/ffmpeg/movie.rb b/lib/ffmpeg/movie.rb index 0e148bb3..2fa59fc6 100644 --- a/lib/ffmpeg/movie.rb +++ b/lib/ffmpeg/movie.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'time' -require 'multi_json' +require 'json' require 'uri' require 'net/http' @@ -37,12 +39,12 @@ def initialize(path) std_error = stderr.read unless stderr.nil? end - fix_encoding(std_output) - fix_encoding(std_error) + FFMPEG.fix_encoding(std_output) + FFMPEG.fix_encoding(std_error) begin - @metadata = MultiJson.load(std_output, symbolize_keys: true) - rescue MultiJson::ParseError + @metadata = JSON.parse(std_output, symbolize_names: true) + rescue JSON::ParserError raise "Could not parse output from FFProbe:\n#{ std_output }" end @@ -51,8 +53,8 @@ def initialize(path) @duration = 0 else - video_streams = @metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'video' } - audio_streams = @metadata[:streams].select { |stream| stream.key?(:codec_type) and stream[:codec_type] === 'audio' } + video_streams = @metadata[:streams].select { |stream| stream.key?(:codec_type) && stream[:codec_type] === 'video' } + audio_streams = @metadata[:streams].select { |stream| stream.key?(:codec_type) && stream[:codec_type] === 'audio' } @container = @metadata[:format][:format_name] @@ -62,7 +64,7 @@ def initialize(path) @format_tags = @metadata[:format][:tags] - @creation_time = if @format_tags and @format_tags.key?(:creation_time) + @creation_time = if @format_tags && @format_tags.key?(:creation_time) begin Time.parse(@format_tags[:creation_time]) rescue ArgumentError @@ -93,23 +95,24 @@ def initialize(path) @video_stream = "#{video_stream[:codec_name]} (#{video_stream[:profile]}) (#{video_stream[:codec_tag_string]} / #{video_stream[:codec_tag]}), #{colorspace}, #{resolution} [SAR #{sar} DAR #{dar}]" - @rotation = if video_stream.key?(:tags) and video_stream[:tags].key?(:rotate) + @rotation = if video_stream.key?(:tags) && video_stream[:tags].key?(:rotate) video_stream[:tags][:rotate].to_i - else - nil + elsif video_stream.key?(:side_data_list) + side_data = video_stream[:side_data_list].find { |sd| sd.key?(:rotation) } + (-side_data[:rotation].to_i) % 360 if side_data end end @audio_streams = audio_streams.map do |stream| { - :index => stream[:index], - :channels => stream[:channels].to_i, - :codec_name => stream[:codec_name], - :sample_rate => stream[:sample_rate].to_i, - :bitrate => stream[:bit_rate].to_i, - :channel_layout => stream[:channel_layout], - :tags => stream[:streams], - :overview => "#{stream[:codec_name]} (#{stream[:codec_tag_string]} / #{stream[:codec_tag]}), #{stream[:sample_rate]} Hz, #{stream[:channel_layout]}, #{stream[:sample_fmt]}, #{stream[:bit_rate]} bit/s" + index: stream[:index], + channels: stream[:channels].to_i, + codec_name: stream[:codec_name], + sample_rate: stream[:sample_rate].to_i, + bitrate: stream[:bit_rate].to_i, + channel_layout: stream[:channel_layout], + tags: stream[:streams], + overview: "#{stream[:codec_name]} (#{stream[:codec_tag_string]} / #{stream[:codec_tag]}), #{stream[:sample_rate]} Hz, #{stream[:channel_layout]}, #{stream[:sample_fmt]}, #{stream[:bit_rate]} bit/s" } end @@ -144,15 +147,15 @@ def unsupported_streams(std_error) end def valid? - not @invalid + !@invalid end def remote? - @path =~ URI::regexp(%w(http https)) + @path.match?(%r{\Ahttps?://}) end def local? - not remote? + !remote? end def width @@ -164,7 +167,7 @@ def height end def resolution - unless width.nil? or height.nil? + unless width.nil? || height.nil? "#{width}x#{height}" end end @@ -199,12 +202,12 @@ def audio_channel_layout end end - def transcode(output_file, options = EncodingOptions.new, transcoder_options = {}, &block) - Transcoder.new(self, output_file, options, transcoder_options).run &block + def transcode(output_file, options = EncodingOptions.new, **transcoder_options, &block) + Transcoder.new(self, output_file, options, transcoder_options).run(&block) end - def screenshot(output_file, options = EncodingOptions.new, transcoder_options = {}, &block) - Transcoder.new(self, output_file, options.merge(screenshot: true), transcoder_options).run &block + def screenshot(output_file, options = EncodingOptions.new, **transcoder_options, &block) + Transcoder.new(self, output_file, options.merge(screenshot: true), transcoder_options).run(&block) end protected @@ -228,18 +231,12 @@ def aspect_from_dimensions aspect.nan? ? nil : aspect end - def fix_encoding(output) - output[/test/] # Running a regexp on the string throws error if it's not UTF-8 - rescue ArgumentError - output.force_encoding("ISO-8859-1") - end - def head(location=@path, limit=FFMPEG.max_http_redirect_attempts) url = URI(location) return unless url.path http = Net::HTTP.new(url.host, url.port) - http.use_ssl = url.port == 443 + http.use_ssl = url.scheme == 'https' response = http.request_head(url.request_uri) case response diff --git a/lib/ffmpeg/transcoder.rb b/lib/ffmpeg/transcoder.rb index f382223d..e4df133c 100644 --- a/lib/ffmpeg/transcoder.rb +++ b/lib/ffmpeg/transcoder.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true + require 'open3' module FFMPEG class Transcoder attr_reader :command, :input - @@timeout = 30 - class << self attr_accessor :timeout end + self.timeout = 30 def initialize(input, output_file, options = EncodingOptions.new, transcoder_options = {}) if input.is_a?(FFMPEG::Movie) @@ -63,13 +64,13 @@ def timeout # frame= 4855 fps= 46 q=31.0 size= 45306kB time=00:02:42.28 bitrate=2287.0kbits/ def transcode_movie FFMPEG.logger.info("Running transcoding...\n#{command}\n") - @output = "" + @output = +"" Open3.popen3(*command) do |_stdin, _stdout, stderr, wait_thr| begin yield(0.0) if block_given? - next_line = Proc.new do |line| - fix_encoding(line) + next_line = lambda do |line| + FFMPEG.fix_encoding(line) @output << line if line.include?("time=") if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style @@ -86,14 +87,14 @@ def transcode_movie end if timeout - stderr.each_with_timeout(wait_thr.pid, timeout, 'size=', &next_line) + each_with_timeout(stderr, wait_thr.pid, timeout, 'size=', &next_line) else stderr.each('size=', &next_line) end @errors << "ffmpeg returned non-zero exit code" unless wait_thr.value.success? - rescue Timeout::Error => e + rescue IO::TimeoutError => e FFMPEG.logger.error "Process hung...\n@command\n#{command}\nOutput\n#{@output}\n" raise Error, "Process hung. Full output: #{@output}" end @@ -133,10 +134,12 @@ def apply_transcoder_options end end - def fix_encoding(output) - output[/test/] - rescue ArgumentError - output.force_encoding("ISO-8859-1") + def each_with_timeout(io, pid, seconds, separator = $/) + io.timeout = seconds + io.each(separator) { |line| yield line } + rescue IO::TimeoutError + Process.kill('KILL', pid) + raise IO::TimeoutError, 'output wait time expired' end def optimize_screenshot_parameters(options, transcoder_options) diff --git a/lib/ffmpeg/version.rb b/lib/ffmpeg/version.rb index 8307d06c..527ae90a 100644 --- a/lib/ffmpeg/version.rb +++ b/lib/ffmpeg/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module FFMPEG VERSION = '3.0.2' end diff --git a/lib/streamio-ffmpeg.rb b/lib/streamio-ffmpeg.rb index 83163784..8e0008ff 100644 --- a/lib/streamio-ffmpeg.rb +++ b/lib/streamio-ffmpeg.rb @@ -1,14 +1,13 @@ -$LOAD_PATH.unshift File.dirname(__FILE__) +# frozen_string_literal: true require 'logger' require 'stringio' -require 'ffmpeg/version' -require 'ffmpeg/errors' -require 'ffmpeg/movie' -require 'ffmpeg/io_monkey' -require 'ffmpeg/transcoder' -require 'ffmpeg/encoding_options' +require_relative 'ffmpeg/version' +require_relative 'ffmpeg/errors' +require_relative 'ffmpeg/movie' +require_relative 'ffmpeg/transcoder' +require_relative 'ffmpeg/encoding_options' module FFMPEG # FFMPEG logs information about its progress when it's transcoding. @@ -24,10 +23,7 @@ def self.logger=(log) # # @return [Logger] def self.logger - return @logger if @logger - logger = Logger.new(STDOUT) - logger.level = Logger::INFO - @logger = logger + @logger ||= Logger.new(STDOUT).tap { |l| l.level = Logger::INFO } end # Set the path of the ffmpeg binary. @@ -38,7 +34,7 @@ def self.logger # @raise Errno::ENOENT if the ffmpeg binary cannot be found def self.ffmpeg_binary=(bin) if bin.is_a?(String) && !File.executable?(bin) - raise Errno::ENOENT, "the ffmpeg binary, \'#{bin}\', is not executable" + raise Errno::ENOENT, "the ffmpeg binary, '#{bin}', is not executable" end @ffmpeg_binary = bin end @@ -48,7 +44,7 @@ def self.ffmpeg_binary=(bin) # @return [String] the path to the ffmpeg binary # @raise Errno::ENOENT if the ffmpeg binary cannot be found def self.ffmpeg_binary - @ffmpeg_binary || which('ffmpeg') + @ffmpeg_binary ||= which('ffmpeg') end # Get the path to the ffprobe binary, defaulting to what is on ENV['PATH'] @@ -56,7 +52,7 @@ def self.ffmpeg_binary # @return [String] the path to the ffprobe binary # @raise Errno::ENOENT if the ffprobe binary cannot be found def self.ffprobe_binary - @ffprobe_binary || which('ffprobe') + @ffprobe_binary ||= which('ffprobe') end # Set the path of the ffprobe binary. @@ -67,7 +63,7 @@ def self.ffprobe_binary # @raise Errno::ENOENT if the ffprobe binary cannot be found def self.ffprobe_binary=(bin) if bin.is_a?(String) && !File.executable?(bin) - raise Errno::ENOENT, "the ffprobe binary, \'#{bin}\', is not executable" + raise Errno::ENOENT, "the ffprobe binary, '#{bin}', is not executable" end @ffprobe_binary = bin end @@ -93,7 +89,6 @@ def self.max_http_redirect_attempts=(v) # Cross-platform way of finding an executable in the $PATH. # # which('ruby') #=> /usr/bin/ruby - # see: http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby def self.which(cmd) exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| @@ -105,4 +100,9 @@ def self.which(cmd) raise Errno::ENOENT, "the #{cmd} binary could not be found in #{ENV['PATH']}" end + def self.fix_encoding(output) + output[/test/] + rescue ArgumentError + output.force_encoding("ISO-8859-1") + end end diff --git a/spec/ffmpeg/encoding_options_spec.rb b/spec/ffmpeg/encoding_options_spec.rb index a9676687..6f98fae2 100644 --- a/spec/ffmpeg/encoding_options_spec.rb +++ b/spec/ffmpeg/encoding_options_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper.rb' module FFMPEG diff --git a/spec/ffmpeg/movie_spec.rb b/spec/ffmpeg/movie_spec.rb index 2e994fc2..8028656c 100644 --- a/spec/ffmpeg/movie_spec.rb +++ b/spec/ffmpeg/movie_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper.rb' require 'net/http' require 'webrick' @@ -381,7 +383,7 @@ module FFMPEG end it "should parse the bitrate" do - expect(movie.bitrate).to eq(481846) + expect(movie.bitrate).to eq(481836) end it "should return nil rotation when no rotation exists" do @@ -512,7 +514,7 @@ module FFMPEG transcoder_double = double(Transcoder) expect(Transcoder).to receive(:new). - with(movie, "#{tmp_path}/awesome.flv", {custom: "-vcodec libx264"}, preserve_aspect_ratio: :width). + with(movie, "#{tmp_path}/awesome.flv", {custom: "-vcodec libx264"}, {preserve_aspect_ratio: :width}). and_return(transcoder_double) expect(transcoder_double).to receive(:run) @@ -527,7 +529,7 @@ module FFMPEG transcoder_double = double(Transcoder) expect(Transcoder).to receive(:new). - with(movie, "#{tmp_path}/awesome.flv", {custom: "-vcodec libx264"}, preserve_aspect_ratio: :width). + with(movie, "#{tmp_path}/awesome.flv", {custom: "-vcodec libx264"}, {preserve_aspect_ratio: :width}). and_return(transcoder_double) expect(transcoder_double).to receive(:run) @@ -546,7 +548,7 @@ module FFMPEG and_return(transcoder_double) expect(transcoder_double).to receive(:run) - movie.transcode("#{tmp_path}/hello.mp3", {audio_codec: 'libmp3lame', custom: %w(-qscale:a 2)}, {}) + movie.transcode("#{tmp_path}/hello.mp3", {audio_codec: 'libmp3lame', custom: %w(-qscale:a 2)}) end end @@ -557,7 +559,7 @@ module FFMPEG transcoder_double = double(Transcoder) expect(Transcoder).to receive(:new). - with(movie, "#{tmp_path}/awesome.jpg", {seek_time: 2, dimensions: "640x480", screenshot: true}, preserve_aspect_ratio: :width). + with(movie, "#{tmp_path}/awesome.jpg", {seek_time: 2, dimensions: "640x480", screenshot: true}, {preserve_aspect_ratio: :width}). and_return(transcoder_double) expect(transcoder_double).to receive(:run) @@ -569,7 +571,7 @@ module FFMPEG transcoder_double = double(Transcoder) expect(Transcoder).to receive(:new). - with(movie, "#{tmp_path}/awesome_%d.jpg", {seek_time: 2, dimensions: '640x480', screenshot: true, vframes: 20}, preserve_aspect_ratio: :width, validate: false). + with(movie, "#{tmp_path}/awesome_%d.jpg", {seek_time: 2, dimensions: '640x480', screenshot: true, vframes: 20}, {preserve_aspect_ratio: :width, validate: false}). and_return(transcoder_double) expect(transcoder_double).to receive(:run) diff --git a/spec/ffmpeg/transcoder_spec.rb b/spec/ffmpeg/transcoder_spec.rb index 41b5d457..ff8ac778 100644 --- a/spec/ffmpeg/transcoder_spec.rb +++ b/spec/ffmpeg/transcoder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper.rb' module FFMPEG @@ -66,7 +68,7 @@ module FFMPEG end it 'should still work with (NTSC target)' do - encoded = Transcoder.new(movie, "#{tmp_path}/awesome.mpg", target: 'ntsc-vcd').run + encoded = Transcoder.new(movie, "#{tmp_path}/awesome.mpg", target: 'ntsc-vcd', duration: 0.5).run expect(encoded.resolution).to eq('352x240') end @@ -76,13 +78,13 @@ module FFMPEG it "should transcode the movie with progress given an awesome movie" do FileUtils.rm_f "#{tmp_path}/awesome.flv" - transcoder = Transcoder.new(movie, "#{tmp_path}/awesome.flv") + transcoder = Transcoder.new(movie, "#{tmp_path}/awesome.flv", duration: 1) progress_updates = [] transcoder.run { |progress| progress_updates << progress } expect(transcoder.encoded).to be_valid expect(progress_updates).to include(0.0, 1.0) expect(progress_updates.length).to be >= 3 - expect(File.exists?("#{tmp_path}/awesome.flv")).to be_truthy + expect(File.exist?("#{tmp_path}/awesome.flv")).to be_truthy end it "should transcode the movie with EncodingOptions" do @@ -107,7 +109,7 @@ module FFMPEG it 'should transcode without video' do FileUtils.rm_f "#{tmp_path}/hello.mp3" - options = { audio_codec: "libmp3lame", custom: %w(-qscale:a 2)} + options = { audio_codec: "libmp3lame", custom: %w(-qscale:a 2), duration: 0.5} encoded = Transcoder.new(sound, "#{tmp_path}/hello.mp3", options).run expect(encoded.video_codec).to be_nil @@ -120,7 +122,7 @@ module FFMPEG context "with aspect ratio preservation" do before do @movie = Movie.new("#{fixture_path}/movies/awesome_widescreen.mov") - @options = {resolution: "320x240"} + @options = {resolution: "320x240", duration: 0.5} end it "should work on width" do @@ -157,7 +159,7 @@ module FFMPEG it "should transcode the movie with String options" do FileUtils.rm_f "#{tmp_path}/string_optionalized.flv" - encoded = Transcoder.new(movie, "#{tmp_path}/string_optionalized.flv", %w(-s 300x200 -ac 2)).run + encoded = Transcoder.new(movie, "#{tmp_path}/string_optionalized.flv", %w(-s 300x200 -ac 2 -t 0.5)).run expect(encoded.resolution).to eq("300x200") expect(encoded.audio_channels).to eq(2) end @@ -167,19 +169,19 @@ module FFMPEG movie = Movie.new("#{fixture_path}/movies/awesome'movie.mov") - expect { Transcoder.new(movie, "#{tmp_path}/output.flv").run }.not_to raise_error + expect { Transcoder.new(movie, "#{tmp_path}/output.flv", duration: 0.5).run }.not_to raise_error end it "should transcode when output filename includes single quotation mark" do FileUtils.rm_f "#{tmp_path}/output with 'quote.flv" - expect { Transcoder.new(movie, "#{tmp_path}/output with 'quote.flv").run }.not_to raise_error + expect { Transcoder.new(movie, "#{tmp_path}/output with 'quote.flv", duration: 0.5).run }.not_to raise_error end it 'should not crash on ISO-8859-1 characters' do FileUtils.rm_f "#{tmp_path}/saløndethé.flv" - expect { Transcoder.new(movie, "#{tmp_path}/saløndethé.flv").run }.not_to raise_error + expect { Transcoder.new(movie, "#{tmp_path}/saløndethé.flv", duration: 0.5).run }.not_to raise_error end it "should fail when given an invalid movie" do @@ -203,7 +205,7 @@ module FFMPEG it "should transcode correctly" do movie = Movie.new("http://127.0.0.1:8000/awesome%20movie.mov") - expect { Transcoder.new(movie, "#{tmp_path}/output.flv").run }.not_to raise_error + expect { Transcoder.new(movie, "#{tmp_path}/output.flv", duration: 0.5).run }.not_to raise_error end end @@ -264,7 +266,7 @@ module FFMPEG context 'with default transcoder_options' do it 'should transcode the movie with the watermark' do - options = { watermark: "#{fixture_path}/images/watermark.png", watermark_filter: { position: 'RB' } } + options = { watermark: "#{fixture_path}/images/watermark.png", watermark_filter: { position: 'RB' }, duration: 0.5 } transcoder = Transcoder.new(movie, "#{tmp_path}/watermarked.mp4", options) expect { transcoder.run }.not_to raise_error end @@ -297,7 +299,8 @@ module FFMPEG context 'with custom options' do let(:options) { { video_codec: 'libx264', - custom: %w(-map 0:0 -map 0:1) + custom: %w(-map 0:0 -map 0:1), + duration: 0.5 } } let(:transcoding_options) { {} } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1c9c2dbe..b9584a62 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,4 @@ -require 'simplecov' -SimpleCov.start +# frozen_string_literal: true require 'bundler' Bundler.require @@ -7,7 +6,6 @@ require 'fileutils' require 'webmock/rspec' WebMock.allow_net_connect! - FFMPEG.logger = Logger.new(nil) RSpec.configure do |config| @@ -16,20 +14,20 @@ config.before(:each) do stub_request(:head, /redirect-example.com/). - with(:headers => {'Accept'=>'*/*', 'User-Agent' => 'Ruby'}). + with(headers: {'Accept'=>'*/*', 'User-Agent' => 'Ruby'}). to_return(status: 302, headers: { location: 'http://127.0.0.1:8000/awesome%20movie.mov' }) stub_request(:head, 'http://127.0.0.1:8000/deep_path/awesome%20movie.mov'). - with(:headers => {'Accept'=>'*/*', 'User-Agent' => 'Ruby'}). + with(headers: {'Accept'=>'*/*', 'User-Agent' => 'Ruby'}). to_return(status: 302, headers: { location: '/awesome%20movie.mov' }) stub_request(:head, 'http://127.0.0.1:8000/awesome%20movie.mov?fail=1'). - with(:headers => {'Accept'=>'*/*', 'User-Agent' => 'Ruby'}). + with(headers: {'Accept'=>'*/*', 'User-Agent' => 'Ruby'}). to_return(status: 404, headers: { }) stub_request(:head, /toomany-redirects-example/). - with(:headers => {'Accept'=>'*/*', 'User-Agent' => 'Ruby'}). + with(headers: {'Accept'=>'*/*', 'User-Agent' => 'Ruby'}). to_return(status: 302, headers: { location: '/awesome%20movie.mov' }) diff --git a/spec/streamio-ffmpeg_spec.rb b/spec/streamio-ffmpeg_spec.rb index 289c396e..c59f901f 100644 --- a/spec/streamio-ffmpeg_spec.rb +++ b/spec/streamio-ffmpeg_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe FFMPEG do @@ -5,16 +7,16 @@ after(:each) do FFMPEG.logger = Logger.new(nil) end - + it "should be a Logger" do expect(FFMPEG.logger).to be_instance_of(Logger) end - + it "should be at info level" do FFMPEG.logger = nil # Reset the logger so that we get the default expect(FFMPEG.logger.level).to eq(Logger::INFO) end - + it "should be assignable" do new_logger = Logger.new(STDOUT) FFMPEG.logger = new_logger @@ -22,15 +24,23 @@ end end - describe '.ffmpeg_binary' do + describe '.which' do + it 'should find an existing executable' do + expect(FFMPEG.which('ffmpeg')).to include('ffmpeg') + end + it 'should raise when executable is not found' do + expect { FFMPEG.which('not_a_real_binary_xyz') }.to raise_error(Errno::ENOENT) + end + end + + describe '.ffmpeg_binary' do after(:each) do FFMPEG.ffmpeg_binary = nil end it 'should default to finding from path' do - allow(FFMPEG).to receive(:which) { '/usr/local/bin/ffmpeg' } - expect(FFMPEG.ffmpeg_binary).to eq FFMPEG.which('ffprobe') + expect(FFMPEG.ffmpeg_binary).to eq FFMPEG.which('ffmpeg') end it 'should be assignable' do @@ -47,17 +57,14 @@ allow(File).to receive(:executable?) { false } expect { FFMPEG.ffmpeg_binary }.to raise_error(Errno::ENOENT) end - end describe '.ffprobe_binary' do - after(:each) do FFMPEG.ffprobe_binary = nil end it 'should default to finding from path' do - allow(FFMPEG).to receive(:which) { '/usr/local/bin/ffprobe' } expect(FFMPEG.ffprobe_binary).to eq FFMPEG.which('ffprobe') end @@ -75,7 +82,6 @@ allow(File).to receive(:executable?) { false } expect { FFMPEG.ffprobe_binary }.to raise_error(Errno::ENOENT) end - end describe '.max_http_redirect_attempts' do @@ -100,4 +106,4 @@ expect(FFMPEG.max_http_redirect_attempts).to eq 5 end end -end \ No newline at end of file +end diff --git a/streamio-ffmpeg.gemspec b/streamio-ffmpeg.gemspec index b0fbbd8b..e02f7fa7 100644 --- a/streamio-ffmpeg.gemspec +++ b/streamio-ffmpeg.gemspec @@ -1,21 +1,19 @@ -# -*- encoding: utf-8 -*- -lib = File.expand_path('../lib/', __FILE__) -$:.unshift lib unless $:.include?(lib) - -require "ffmpeg/version" +require_relative "lib/ffmpeg/version" Gem::Specification.new do |s| s.name = "streamio-ffmpeg" s.version = FFMPEG::VERSION s.authors = ["Rackfish AB"] s.email = ["support@rackfish.com", "bikeath1337.com"] - s.homepage = "http://github.com/streamio/streamio-ffmpeg" + s.homepage = "https://github.com/streamio/streamio-ffmpeg" s.summary = "Wraps ffmpeg to read metadata and transcodes videos." + s.license = "MIT" + + s.metadata["allowed_push_host"] = "https://rubygems.org" - s.add_dependency('multi_json', '~> 1.8') + s.required_ruby_version = ">= 3.3" - s.add_development_dependency("rspec", "~> 3") - s.add_development_dependency("rake", "~> 10.1") + s.add_dependency "logger" - s.files = Dir.glob("lib/**/*") + %w(README.md LICENSE CHANGELOG) + s.files = Dir.glob("lib/**/*") + %w(README.md LICENSE CHANGELOG) end