From 7489db367b4d59ad4f2ad9c8a2632b947ceff168 Mon Sep 17 00:00:00 2001 From: Giovanni Cangiani Date: Tue, 1 Dec 2015 15:53:13 +0100 Subject: [PATCH 1/2] Adds transcoding to multiple output at once --- README.md | 9 +++ lib/ffmpeg/movie.rb | 17 ++++- lib/ffmpeg/transcoder.rb | 127 +++++++++++++++++++++------------ spec/ffmpeg/movie_spec.rb | 12 ++++ spec/ffmpeg/transcoder_spec.rb | 23 ++++++ 5 files changed, 142 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 93ce27b1..f431e3de 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,15 @@ options = {video_min_bitrate: 600, video_max_bitrate: 600, buffer_size: 2000} movie.transcode("movie.flv", options) ``` +Transcode to multiple output files within the same call to ffmpeg + +```ruby +options = { resolution: "320x240" } +movie.enqueue_transcoding("#{tmp_path}/awesome.flv", options) +movie.enqueue_transcoding("#{tmp_path}/durationalized.mp4", options) +flv_movie, mp4_movie = movie.transcode_queue +``` + Add watermark image on the video. For example, you want to add a watermark on the video at right top corner with 10px padding. diff --git a/lib/ffmpeg/movie.rb b/lib/ffmpeg/movie.rb index cc04861b..8e92bbb1 100644 --- a/lib/ffmpeg/movie.rb +++ b/lib/ffmpeg/movie.rb @@ -35,7 +35,7 @@ def initialize(path) output[/rotate\ {1,}:\ {1,}(\d*)/] @rotation = $1 ? $1.to_i : nil - + output[/Video:\ (.*)/] @video_stream = $1 @@ -60,6 +60,8 @@ def initialize(path) @invalid = true if @video_stream.to_s.empty? && @audio_stream.to_s.empty? @invalid = true if output.include?("is not supported") @invalid = true if output.include?("could not find codec parameters") + + @transcoder = nil end def valid? @@ -103,6 +105,19 @@ def transcode(output_file, options = EncodingOptions.new, transcoder_options = { Transcoder.new(self, output_file, options, transcoder_options).run &block end + def enqueue_transcoding(output_file, options = EncodingOptions.new, transcoder_options = {}) + if @transcoder.nil? + @transcoder = Transcoder.new(self, output_file, options, transcoder_options) + else + @transcoder.append(output_file, options, transcoder_options) + end + end + + def transcode_queue + return unless @transcoder + @transcoder.run + end + def screenshot(output_file, options = EncodingOptions.new, transcoder_options = {}, &block) Transcoder.new(self, output_file, options.merge(screenshot: true), transcoder_options).run &block end diff --git a/lib/ffmpeg/transcoder.rb b/lib/ffmpeg/transcoder.rb index 1d27f899..ee32812c 100644 --- a/lib/ffmpeg/transcoder.rb +++ b/lib/ffmpeg/transcoder.rb @@ -2,19 +2,11 @@ require 'shellwords' module FFMPEG - class Transcoder - @@timeout = 30 + class TranscoderParams - def self.timeout=(time) - @@timeout = time - end - - def self.timeout - @@timeout - end + attr_reader :output_file, :raw_options, :transcoder_options, :errors def initialize(movie, output_file, options = EncodingOptions.new, transcoder_options = {}) - @movie = movie @output_file = output_file if options.is_a?(String) || options.is_a?(EncodingOptions) @@ -26,23 +18,14 @@ def initialize(movie, output_file, options = EncodingOptions.new, transcoder_opt end @transcoder_options = transcoder_options - @errors = [] - apply_transcoder_options - end + @errors = [] - def run(&block) - transcode_movie(&block) - if @transcoder_options[:validate] - validate_output_file(&block) - return encoded - else - return nil - end + apply_transcoder_options(movie) end def encoding_succeeded? - @errors << "no output file created" and return false unless File.exists?(@output_file) + @errors << "no output file created" and return false unless File.exists?(output_file) @errors << "encoded file is invalid" and return false unless encoded.valid? true end @@ -52,9 +35,81 @@ def encoded end private + + def apply_transcoder_options(movie) + # if true runs #validate_output_file + @transcoder_options[:validate] = @transcoder_options.fetch(:validate) { true } + + return if movie.calculated_aspect_ratio.nil? + case @transcoder_options[:preserve_aspect_ratio].to_s + when "width" + new_height = @raw_options.width / movie.calculated_aspect_ratio + new_height = new_height.ceil.even? ? new_height.ceil : new_height.floor + new_height += 1 if new_height.odd? # needed if new_height ended up with no decimals in the first place + @raw_options[:resolution] = "#{@raw_options.width}x#{new_height}" + when "height" + new_width = @raw_options.height * movie.calculated_aspect_ratio + new_width = new_width.ceil.even? ? new_width.ceil : new_width.floor + new_width += 1 if new_width.odd? + @raw_options[:resolution] = "#{new_width}x#{@raw_options.height}" + end + end + + end + + class Transcoder + @@timeout = 30 + + def self.timeout=(time) + @@timeout = time + end + + def self.timeout + @@timeout + end + + def initialize(movie, output_file, options = EncodingOptions.new, transcoder_options = {}) + @movie = movie + @params_hash = {} + append(output_file, options, transcoder_options) + end + + def params + @params_hash.values + end + + def append(output_file, options = EncodingOptions.new, transcoder_options = {}) + @params_hash[output_file] = TranscoderParams.new(@movie, output_file, options, transcoder_options) + end + + def run(&block) + transcode_movie(&block) + out=[] + params.each do |p| + if p.transcoder_options[:validate] + validate_output_file(p, &block) + out << p.encoded + else + out << nil + end + end + out.length == 1 ? out.first : out + end + + def encoded + out = params.map {|p| p.encoded} + out.length == 1 ? out.first : out + end + + private + # frame= 4855 fps= 46 q=31.0 size= 45306kB time=00:02:42.28 bitrate=2287.0kbits/ def transcode_movie - @command = "#{FFMPEG.ffmpeg_binary} -y -i #{Shellwords.escape(@movie.path)} #{@raw_options} #{Shellwords.escape(@output_file)}" + @command = "#{FFMPEG.ffmpeg_binary} -y -i #{Shellwords.escape(@movie.path)} " + params.each do |p| + @command << " #{p.raw_options} #{Shellwords.escape(p.output_file)}" + end + FFMPEG.logger.info("Running transcoding...\n#{@command}\n") @output = "" @@ -88,35 +143,17 @@ def transcode_movie end end - def validate_output_file(&block) - if encoding_succeeded? + def validate_output_file(tcparam, &block) + if tcparam.encoding_succeeded? yield(1.0) if block_given? - FFMPEG.logger.info "Transcoding of #{@movie.path} to #{@output_file} succeeded\n" + FFMPEG.logger.info "Transcoding of #{@movie.path} to #{tcparam.output_file} succeeded\n" else - errors = "Errors: #{@errors.join(", ")}. " + errors = "Errors: #{tcparam.errors.join(", ")}. " FFMPEG.logger.error "Failed encoding...\n#{@command}\n\n#{@output}\n#{errors}\n" raise Error, "Failed encoding.#{errors}Full output: #{@output}" end end - def apply_transcoder_options - # if true runs #validate_output_file - @transcoder_options[:validate] = @transcoder_options.fetch(:validate) { true } - - return if @movie.calculated_aspect_ratio.nil? - case @transcoder_options[:preserve_aspect_ratio].to_s - when "width" - new_height = @raw_options.width / @movie.calculated_aspect_ratio - new_height = new_height.ceil.even? ? new_height.ceil : new_height.floor - new_height += 1 if new_height.odd? # needed if new_height ended up with no decimals in the first place - @raw_options[:resolution] = "#{@raw_options.width}x#{new_height}" - when "height" - new_width = @raw_options.height * @movie.calculated_aspect_ratio - new_width = new_width.ceil.even? ? new_width.ceil : new_width.floor - new_width += 1 if new_width.odd? - @raw_options[:resolution] = "#{new_width}x#{@raw_options.height}" - end - end def fix_encoding(output) output[/test/] diff --git a/spec/ffmpeg/movie_spec.rb b/spec/ffmpeg/movie_spec.rb index 4f26a12c..8ad19738 100644 --- a/spec/ffmpeg/movie_spec.rb +++ b/spec/ffmpeg/movie_spec.rb @@ -393,5 +393,17 @@ module FFMPEG movie.screenshot("#{tmp_path}/awesome.jpg", {seek_time: 2, dimensions: "640x480"}, preserve_aspect_ratio: :width) end end + + describe "transcode to multiple output" do + it "should be able to enqueue multiple multiple transcoding outputs" do + movie = Movie.new("#{fixture_path}/movies/awesome movie.mov") + movie.enqueue_transcoding("#{tmp_path}/awesome.flv", duration: 2) + movie.enqueue_transcoding("#{tmp_path}/durationalized.mp4", duration: 2) + o1, o2 = movie.transcode_queue + o1.should be_valid + o2.should be_valid + end + end + end end diff --git a/spec/ffmpeg/transcoder_spec.rb b/spec/ffmpeg/transcoder_spec.rb index 6bf9b0d7..2b42eebf 100644 --- a/spec/ffmpeg/transcoder_spec.rb +++ b/spec/ffmpeg/transcoder_spec.rb @@ -169,6 +169,29 @@ module FFMPEG encoded.duration.should <= 2.2 end + it "should encode with multiple output at once" do + transcoder = Transcoder.new(movie, "#{tmp_path}/durationalized.mp4", duration: 2) + transcoder.append("#{tmp_path}/output.flv") + + encoded1 = encoded2 = nil + expect { encoded1, encoded2 = transcoder.run }.not_to raise_error + + encoded1.duration.should >= 1.8 + encoded1.duration.should <= 2.2 + end + + it "should keep only latest enqueued transcoding for a given output path" do + transcoder = Transcoder.new(movie, "#{tmp_path}/durationalized.mp4", duration: 4) + transcoder.append("#{tmp_path}/durationalized.mp4", duration: 2) + + encoded = nil + expect { encoded = transcoder.run }.not_to raise_error + encoded.class.should == FFMPEG::Movie + encoded.duration.should >= 1.8 + encoded.duration.should <= 2.2 + end + + context "with screenshot option" do it "should transcode to original movies resolution by default" do encoded = Transcoder.new(movie, "#{tmp_path}/image.jpg", screenshot: true).run From 87b3d2bb77785d7c2aa5aacb6985034d908939c9 Mon Sep 17 00:00:00 2001 From: Giovanni Cangiani Date: Tue, 1 Dec 2015 16:01:07 +0100 Subject: [PATCH 2/2] Adds block also for transcode_queue --- lib/ffmpeg/movie.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ffmpeg/movie.rb b/lib/ffmpeg/movie.rb index 8e92bbb1..6e830416 100644 --- a/lib/ffmpeg/movie.rb +++ b/lib/ffmpeg/movie.rb @@ -113,9 +113,9 @@ def enqueue_transcoding(output_file, options = EncodingOptions.new, transcoder_o end end - def transcode_queue + def transcode_queue(&block) return unless @transcoder - @transcoder.run + @transcoder.run &block end def screenshot(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)