Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat multiout #110

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 16 additions & 1 deletion lib/ffmpeg/movie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def initialize(path)

output[/rotate\ {1,}:\ {1,}(\d*)/]
@rotation = $1 ? $1.to_i : nil

output[/Video:\ (.*)/]
@video_stream = $1

Expand All @@ -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?
Expand Down Expand Up @@ -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(&block)
return unless @transcoder
@transcoder.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
end
Expand Down
127 changes: 82 additions & 45 deletions lib/ffmpeg/transcoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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 = ""

Expand Down Expand Up @@ -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/]
Expand Down
12 changes: 12 additions & 0 deletions spec/ffmpeg/movie_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 23 additions & 0 deletions spec/ffmpeg/transcoder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down