diff --git a/.gitignore b/.gitignore index ea57e44a..80039820 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ tmtags ## VIM *.swp +## RubyMine +.idea/ + ## PROJECT::GENERAL coverage rdoc diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..8d7b70b7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +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 590cbf4a..cf74b064 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,63 @@ == Master +Current with 3.0.2 + +== 3.0.2 2016-11-18 + +Improvements: +* Issue #153 Adds support for multiple audio streams +* Issue #150 Does not trust reported SAR/DAR as these may not be defined per https://trac.ffmpeg.org/ticket/3798 + +== 3.0.1 2016-11-10 + +Improvements: +* Issue #144 is fixed. Of a nil movie is presented to transcode, the progress block does not fail +* Issue #145 Adds ability to follow URLs when presented as Movie inputs + + +== 3.0.0 2016-09-07 + +Breaking Changes: +* Changes: custom parameters now specified using an array syntax + +Improvements: +* Issue # 128, #137 are fixed (custom parameters passed as strings) which was caused + by 2.1.0 in attempting to avoid the use of shell and filename escaping issues + on various platforms. + + +== 2.1.0 2016-08-08 + +New: +* Issue #135, Support creating slideshows from stills +* Issue #130, Adds support for a URL as an input +* Support for specifying options to apply to the input file +* Improved support for calling FFMpeg with popen3, improving compatibility with various platforms, including windows +* Issue #111. uses attr_accessor for the timeout attribute +* Exposes the movie's entire meta data and format tags as movie attributes +* Issue #122, adds encoding fix to stderr +* All specs pass and brought current with Rspec V3 +* Issue #123, #131 improved stream validation, ignoring additional invalid streams if a valid stream is recognized +* Issue #124, improved parsing for moving creation_time +* SAR and DAR now adapt after rotation, DAR is ignored when calculating dimensions + +Improvements: +* Allow parenthesis in colorspace (thanks walterdavis for initial code and rociiu for finding a bug with it) +* Width and height now switched if video is filmed in portrait mode. ffmpeg 2.7 or later now automatically rotates output +* Movie metadata now provided with ffprobe (#114). Thanks to Ryan Lovelett for the contributions! +* Ability to create multiple screenshots at consistent intervals in one pass (#113) + +== 2.0.0 2016-01-14 + New: * Support watermarking (thanks smoothdvd) +* Bumped target ffmpeg version to 2.8 Improvements: * Allow parenthesis in colorspace (thanks walterdavis for initial code and rociiu for finding a bug with it) +* Width and height now switched if video is filmed in portrait mode. ffmpeg 2.7 or later now automatically rotates output +* Movie metadata now provided with ffprobe (#114). Thanks to Ryan Lovelett for the contributions! +* Ability to create multiple screenshots at consistent intervals in one pass (#113) == 1.0.0 2013-07-08 diff --git a/Gemfile b/Gemfile index b4e2a20b..72aeda35 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,8 @@ source "https://rubygems.org" gemspec + +group :test do + gem 'webmock' + gem 'simplecov' +end \ No newline at end of file diff --git a/README.md b/README.md index 93ce27b1..1fa333f1 100644 --- a/README.md +++ b/README.md @@ -1,6 +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](http://streamio.com). @@ -10,7 +14,7 @@ All work on this project is sponsored by the online video platform [Streamio](ht Installation ------------ - (sudo) gem install streamio-ffmpeg + gem install streamio-ffmpeg Compatibility ------------- @@ -23,7 +27,11 @@ Will not work in jruby until they fix: http://goo.gl/Z4UcX (should work in the u ### ffmpeg -The current gem is tested against ffmpeg 1.2.1. 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. +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`. Usage ----- @@ -31,7 +39,6 @@ Usage ### Require the gem ``` ruby -require 'rubygems' require 'streamio-ffmpeg' ``` @@ -57,6 +64,9 @@ movie.audio_codec # "aac" movie.audio_sample_rate # 44100 movie.audio_channels # 2 +# Multiple audio streams +movie.audio_streams[0] # "aac, 44100 Hz, stereo, s16, 75 kb/s" (raw audio stream info) + movie.valid? # true (would be false if ffmpeg fails to read the movie) ``` @@ -74,21 +84,24 @@ Keep track of progress with an optional block. movie.transcode("movie.mp4") { |progress| puts progress } # 0.2 ... 0.5 ... 1.0 ``` -Give custom command line options with a string. +Give custom command line options with an array. ``` ruby -movie.transcode("movie.mp4", "-ac aac -vc libx264 -ac 2 ...") +movie.transcode("movie.mp4", %w(-ac aac -vc libx264 -ac 2 ...)) ``` -Use the EncodingOptions parser for humanly readable transcoding options. Below you'll find most of the supported options. Note that the :custom key will be used as is without modification so use it for any tricky business you might need. +Use the EncodingOptions parser for humanly readable transcoding options. Below you'll find most of the supported options. +Note that the :custom key is an array so that it can be used for FFMpeg options like +`-map` that can be repeated: ``` ruby -options = {video_codec: "libx264", frame_rate: 10, resolution: "320x240", video_bitrate: 300, video_bitrate_tolerance: 100, - aspect: 1.333333, keyframe_interval: 90, - x264_vprofile: "high", x264_preset: "slow", - audio_codec: "libfaac", audio_bitrate: 32, audio_sample_rate: 22050, audio_channels: 1, - threads: 2, - custom: "-vf crop=60:60:10:10"} +options = { + video_codec: "libx264", frame_rate: 10, resolution: "320x240", video_bitrate: 300, video_bitrate_tolerance: 100, + aspect: 1.333333, keyframe_interval: 90, x264_vprofile: "high", x264_preset: "slow", + audio_codec: "libfaac", audio_bitrate: 32, audio_sample_rate: 22050, audio_channels: 1, + threads: 2, custom: %w(-vf crop=60:60:10:10 -map 0:0 -map 0:1) +} + movie.transcode("movie.mp4", options) ``` @@ -128,15 +141,52 @@ options = {video_min_bitrate: 600, video_max_bitrate: 600, buffer_size: 2000} 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. + +``` ruby +movie = FFMPEG::Movie.new("path/to/movie.mov") + +transcoder_options = { input_options: { framerate: '1/5' } } +movie.transcode("movie.mp4", {}, transcoder_options) + +# FFMPEG Command will look like this: +# ffmpeg -y -framerate 1/5 -i path/to/movie.mov movie.mp4 +``` + +### 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. + +``` ruby +movie = FFMPEG::Movie.new("path/to/movie.mov") + +transcoder_options = { input: 'img_%03d.png' } +movie.transcode("movie.mp4", {}, transcoder_options) + +# FFMPEG Command will look like this: +# ffmpeg -y -i img_%03d.png movie.mp4 +``` + +### Watermarking + Add watermark image on the video. For example, you want to add a watermark on the video at right top corner with 10px padding. ``` ruby -options = { watermark: "full_path_of_watermark.png", resolution: "640x360", watermark_filter: { position: "RT", padding_x: 10, padding_y: 10 } } +options = { + watermark: "full_path_of_watermark.png", resolution: "640x360", + watermark_filter: { position: "RT", padding_x: 10, padding_y: 10 } +} ``` Position can be "LT" (Left Top Corner), "RT" (Right Top Corner), "LB" (Left Bottom Corner), "RB" (Right Bottom Corner). +The watermark will not appear unless `watermark_options` specifies the position. `padding_x` and `padding_y` default to +`10`. ### Taking Screenshots @@ -152,28 +202,61 @@ The screenshot method has the very same API as transcode so the same options wil movie.screenshot("screenshot.bmp", seek_time: 5, resolution: '320x240') ``` +To generate multiple screenshots in a single pass, specify `vframes` and a wildcard filename. Make +sure to disable output file validation. The following code generates up to 20 screenshots every 10 seconds: + +``` ruby +movie.screenshot("screenshot_%d.jpg", { vframes: 20, frame_rate: '1/6' }, validate: false) +``` + +To specify the quality when generating compressed screenshots (.jpg), use `quality` which specifies +ffmpeg `-v:q` option. Quality is an integer between 1 and 31, where lower is better quality: + +``` ruby +movie.screenshot("screenshot_%d.jpg", quality: 3) +``` + You can preserve aspect ratio the same way as when using transcode. ``` ruby movie.screenshot("screenshot.png", { seek_time: 2, resolution: '200x120' }, preserve_aspect_ratio: :width) ``` +### Create a Slideshow from Stills +Creating a slideshow from stills uses named sequences of files and stiches the result together in a slideshow +video. + +Since there is not movie to transcode, the Transcoder class needs to be used. The input and input_options are +provided through transcoder options. + +``` ruby +slideshow_transcoder = FFMPEG::Transcoder.new( + '', + 'slideshow.mp4', + { resolution: "320x240" }, + input: 'img_%03d.jpeg', + input_options: { framerate: '1/5' } +) + +slideshow = slideshow_transcoder.run +``` + Specify the path to ffmpeg -------------------------- -By default, streamio 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 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: ``` ruby FFMPEG.ffmpeg_binary = '/usr/local/bin/ffmpeg' ``` -This will cause the same command to run as "/usr/local/bin/ffmpeg -i /path/to/input.file ..." instead. +This will cause the same command to run as `/usr/local/bin/ffmpeg -i /path/to/input.file ...` instead. Automatically kill hung processes --------------------------------- -By default, streamio will wait for 30 seconds between IO feedback from the FFMPEG process. After which an error is logged and the process killed. +By default, the gem will wait for 30 seconds between IO feedback from the FFMPEG process. After which an error is logged and the process killed. It is possible to modify this behaviour by setting a new default: ``` ruby @@ -199,7 +282,6 @@ transcoder_options = { validate: false } movie.transcode("movie.mp4", options, transcoder_options) # returns nil ``` - Copyright --------- diff --git a/lib/ffmpeg/encoding_options.rb b/lib/ffmpeg/encoding_options.rb index be087908..4bbf2369 100644 --- a/lib/ffmpeg/encoding_options.rb +++ b/lib/ffmpeg/encoding_options.rb @@ -4,21 +4,34 @@ def initialize(options = {}) merge!(options) end - def to_s - params = collect do |key, value| - send("convert_#{key}", value) if value && supports_option?(key) + def params_order(k) + if k =~ /watermark$/ + 0 + elsif k =~ /watermark/ + 1 + elsif k =~ /codec/ + 2 + elsif k =~ /preset/ + 3 + else + 4 end + end + + def to_a + params = [] # codecs should go before the presets so that the files will be matched successfully # all other parameters go after so that we can override whatever is in the preset - codecs = params.select { |p| p =~ /codec/ } - presets = params.select { |p| p =~ /\-.pre/ } - other = params - codecs - presets - params = codecs + presets + other + keys.sort_by{|k| params_order(k) }.each do |key| + + value = self[key] + a = send("convert_#{key}", value) if value && supports_option?(key) + params += a unless a.nil? + end - params_string = params.join(" ") - params_string << " #{convert_aspect(calculate_aspect)}" if calculate_aspect? - params_string + params += convert_aspect(calculate_aspect) if calculate_aspect? + params.map(&:to_s) end def width @@ -36,7 +49,7 @@ def supports_option?(option) end def convert_aspect(value) - "-aspect #{value}" + ["-aspect", value] end def calculate_aspect @@ -49,111 +62,134 @@ def calculate_aspect? end def convert_video_codec(value) - "-vcodec #{value}" + ["-vcodec", value] end def convert_frame_rate(value) - "-r #{value}" + ["-r", value] end def convert_resolution(value) - "-s #{value}" + ["-s", value] end def convert_video_bitrate(value) - "-b:v #{k_format(value)}" + ["-b:v", k_format(value)] end def convert_audio_codec(value) - "-acodec #{value}" + ["-acodec", value] end def convert_audio_bitrate(value) - "-b:a #{k_format(value)}" + ["-b:a", k_format(value)] end def convert_audio_sample_rate(value) - "-ar #{value}" + ["-ar", value] end def convert_audio_channels(value) - "-ac #{value}" + ["-ac", value] end def convert_video_max_bitrate(value) - "-maxrate #{k_format(value)}" + ["-maxrate", k_format(value)] end def convert_video_min_bitrate(value) - "-minrate #{k_format(value)}" + ["-minrate", k_format(value)] end def convert_buffer_size(value) - "-bufsize #{k_format(value)}" + ["-bufsize", k_format(value)] end def convert_video_bitrate_tolerance(value) - "-bt #{k_format(value)}" + ["-bt", k_format(value)] end def convert_threads(value) - "-threads #{value}" + ["-threads", value] + end + + def convert_target(value) + ['-target', value] end def convert_duration(value) - "-t #{value}" + ["-t", value] end def convert_video_preset(value) - "-vpre #{value}" + ["-vpre", value] end def convert_audio_preset(value) - "-apre #{value}" + ["-apre", value] end def convert_file_preset(value) - "-fpre #{value}" + ["-fpre", value] end def convert_keyframe_interval(value) - "-g #{value}" + ["-g", value] end def convert_seek_time(value) - "-ss #{value}" + ["-ss", value] end def convert_screenshot(value) - value ? "-vframes 1 -f image2" : "" + result = [] + unless self[:vframes] + result << '-vframes' + result << 1 + end + result << '-f' + result << 'image2' + value ? result : [] + end + + def convert_quality(value) + ['-q:v', value] + end + + def convert_vframes(value) + ['-vframes', value] end def convert_x264_vprofile(value) - "-vprofile #{value}" + ["-vprofile", value] end def convert_x264_preset(value) - "-preset #{value}" + ["-preset", value] end def convert_watermark(value) - "-i #{value}" + ["-i", value] end def convert_watermark_filter(value) - case value[:position].to_s - when "LT" - "-filter_complex 'scale=#{self[:resolution]},overlay=x=#{value[:padding_x]}:y=#{value[:padding_y]}'" - when "RT" - "-filter_complex 'scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{value[:padding_x]}:y=#{value[:padding_y]}'" - when "LB" - "-filter_complex 'scale=#{self[:resolution]},overlay=x=#{value[:padding_x]}:y=main_h-overlay_h-#{value[:padding_y]}'" - when "RB" - "-filter_complex 'scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{value[:padding_x]}:y=main_h-overlay_h-#{value[:padding_y]}'" - end + position = value[:position] + padding_x = value[:padding_x] || 10 + padding_y = value[:padding_y] || 10 + case position.to_s + when "LT" + ["-filter_complex", "scale=#{self[:resolution]},overlay=x=#{padding_x}:y=#{padding_y}"] + when "RT" + ["-filter_complex", "scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{padding_x}:y=#{padding_y}"] + when "LB" + ["-filter_complex", "scale=#{self[:resolution]},overlay=x=#{padding_x}:y=main_h-overlay_h-#{padding_y}"] + when "RB" + ["-filter_complex", "scale=#{self[:resolution]},overlay=x=main_w-overlay_w-#{padding_x}:y=main_h-overlay_h-#{padding_y}"] + end end def convert_custom(value) + raise ArgumentError unless value.class <= Array value end diff --git a/lib/ffmpeg/errors.rb b/lib/ffmpeg/errors.rb index 64b79a7c..ac862d7d 100644 --- a/lib/ffmpeg/errors.rb +++ b/lib/ffmpeg/errors.rb @@ -1,4 +1,6 @@ module FFMPEG class Error < StandardError end + class HTTPTooManyRequests < StandardError + end end diff --git a/lib/ffmpeg/movie.rb b/lib/ffmpeg/movie.rb index 5f4fbf7d..56a13548 100644 --- a/lib/ffmpeg/movie.rb +++ b/lib/ffmpeg/movie.rb @@ -1,77 +1,169 @@ require 'time' +require 'multi_json' +require 'uri' module FFMPEG class Movie attr_reader :path, :duration, :time, :bitrate, :rotation, :creation_time - attr_reader :video_stream, :video_codec, :video_bitrate, :colorspace, :resolution, :sar, :dar - attr_reader :audio_stream, :audio_codec, :audio_bitrate, :audio_sample_rate + attr_reader :video_stream, :video_codec, :video_bitrate, :colorspace, :width, :height, :sar, :dar, :frame_rate + attr_reader :audio_streams, :audio_stream, :audio_codec, :audio_bitrate, :audio_sample_rate, :audio_channels, :audio_tags attr_reader :container + attr_reader :metadata, :format_tags - def initialize(path) - raise Errno::ENOENT, "the file '#{path}' does not exist" unless File.exists?(path) + UNSUPPORTED_CODEC_PATTERN = /^Unsupported codec with id (\d+) for input stream (\d+)$/ + def initialize(path) @path = path - # ffmpeg will output to stderr - command = "#{FFMPEG.ffmpeg_binary} -i #{Shellwords.escape(path)}" - output = Open3.popen3(command) { |stdin, stdout, stderr| stderr.read } - - fix_encoding(output) + if remote? + @head = head + raise Errno::ENOENT, "the URL '#{path}' does not exist" unless @head.is_a?(Net::HTTPSuccess) + else + raise Errno::ENOENT, "the file '#{path}' does not exist" unless File.exist?(path) + end - output[/Input \#\d+\,\s*(\S+),\s*from/] - @container = $1 + @path = path - output[/Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})/] - @duration = ($1.to_i*60*60) + ($2.to_i*60) + $3.to_f + # ffmpeg will output to stderr + command = [FFMPEG.ffprobe_binary, '-i', path, *%w(-print_format json -show_format -show_streams -show_error)] + std_output = '' + std_error = '' - output[/start: (\d*\.\d*)/] - @time = $1 ? $1.to_f : 0.0 + Open3.popen3(*command) do |stdin, stdout, stderr| + std_output = stdout.read unless stdout.nil? + std_error = stderr.read unless stderr.nil? + end - output[/creation_time {1,}: {1,}(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/] - @creation_time = $1 ? Time.parse("#{$1}") : nil + fix_encoding(std_output) + fix_encoding(std_error) - output[/bitrate: (\d*)/] - @bitrate = $1 ? $1.to_i : nil + begin + @metadata = MultiJson.load(std_output, symbolize_keys: true) + rescue MultiJson::ParseError + raise "Could not parse output from FFProbe:\n#{ std_output }" + end - output[/rotate\ {1,}:\ {1,}(\d*)/] - @rotation = $1 ? $1.to_i : nil + if @metadata.key?(:error) + + @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' } + + @container = @metadata[:format][:format_name] + + @duration = @metadata[:format][:duration].to_f + + @time = @metadata[:format][:start_time].to_f + + @format_tags = @metadata[:format][:tags] + + @creation_time = if @format_tags and @format_tags.key?(:creation_time) + begin + Time.parse(@format_tags[:creation_time]) + rescue ArgumentError + nil + end + else + nil + end + + @bitrate = @metadata[:format][:bit_rate].to_i + + # TODO: Handle multiple video codecs (is that possible?) + video_stream = video_streams.first + unless video_stream.nil? + @video_codec = video_stream[:codec_name] + @colorspace = video_stream[:pix_fmt] + @width = video_stream[:width] + @height = video_stream[:height] + @video_bitrate = video_stream[:bit_rate].to_i + @sar = video_stream[:sample_aspect_ratio] + @dar = video_stream[:display_aspect_ratio] + + @frame_rate = unless video_stream[:avg_frame_rate] == '0/0' + Rational(video_stream[:avg_frame_rate]) + else + nil + end + + @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) + video_stream[:tags][:rotate].to_i + else + nil + 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" + } + end + + audio_stream = @audio_streams.first + unless audio_stream.nil? + @audio_channels = audio_stream[:channels] + @audio_codec = audio_stream[:codec_name] + @audio_sample_rate = audio_stream[:sample_rate] + @audio_bitrate = audio_stream[:bitrate] + @audio_channel_layout = audio_stream[:channel_layout] + @audio_tags = audio_stream[:audio_tags] + @audio_stream = audio_stream[:overview] + end - output[/Video:\ (.*)/] - @video_stream = $1 + end - output[/Audio:\ (.*)/] - @audio_stream = $1 + unsupported_stream_ids = unsupported_streams(std_error) + nil_or_unsupported = -> (stream) { stream.nil? || unsupported_stream_ids.include?(stream[:index]) } - if video_stream - commas_except_in_parenthesis = /(?:\([^()]*\)|[^,])+/ # regexp to handle "yuv420p(tv, bt709)" colorspace etc from http://goo.gl/6oi645 - @video_codec, @colorspace, resolution, video_bitrate = video_stream.scan(commas_except_in_parenthesis).map(&:strip) - @video_bitrate = video_bitrate =~ %r(\A(\d+) kb/s\Z) ? $1.to_i : nil - @resolution = resolution.split(" ").first rescue nil # get rid of [PAR 1:1 DAR 16:9] - @sar = $1 if video_stream[/SAR (\d+:\d+)/] - @dar = $1 if video_stream[/DAR (\d+:\d+)/] - end + @invalid = true if nil_or_unsupported.(video_stream) && nil_or_unsupported.(audio_stream) + @invalid = true if @metadata.key?(:error) + @invalid = true if std_error.include?("could not find codec parameters") + end - if audio_stream - @audio_codec, audio_sample_rate, @audio_channels, unused, audio_bitrate = audio_stream.split(/\s?,\s?/) - @audio_bitrate = audio_bitrate =~ %r(\A(\d+) kb/s\Z) ? $1.to_i : nil - @audio_sample_rate = audio_sample_rate[/\d*/].to_i + def unsupported_streams(std_error) + [].tap do |stream_indices| + std_error.each_line do |line| + match = line.match(UNSUPPORTED_CODEC_PATTERN) + stream_indices << match[2].to_i if match + end end - - @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") end def valid? not @invalid end + def remote? + @path =~ URI::regexp(%w(http https)) + end + + def local? + not remote? + end + def width - resolution.split("x")[0].to_i rescue nil + rotation.nil? || rotation == 180 ? @width : @height; end def height - resolution.split("x")[1].to_i rescue nil + rotation.nil? || rotation == 180 ? @height : @width; + end + + def resolution + unless width.nil? or height.nil? + "#{width}x#{height}" + end end def calculated_aspect_ratio @@ -83,20 +175,25 @@ def calculated_pixel_aspect_ratio end def size - File.size(@path) - end - - def audio_channels - return nil unless @audio_channels - return @audio_channels[/\d*/].to_i if @audio_channels["channels"] - return 1 if @audio_channels["mono"] - return 2 if @audio_channels["stereo"] - return 6 if @audio_channels["5.1"] + if local? + File.size(@path) + else + @head.content_length + end end - def frame_rate - return nil unless video_stream - video_stream[/(\d*\.?\d*)\s?fps/] ? $1.to_f : nil + def audio_channel_layout + # TODO Whenever support for ffmpeg/ffprobe 1.2.1 is dropped this is no longer needed + @audio_channel_layout || case(audio_channels) + when 1 + 'stereo' + when 2 + 'stereo' + when 6 + '5.1' + else + 'unknown' + end end def transcode(output_file, options = EncodingOptions.new, transcoder_options = {}, &block) @@ -109,17 +206,18 @@ def screenshot(output_file, options = EncodingOptions.new, transcoder_options = protected def aspect_from_dar - return nil unless dar - w, h = dar.split(":") - aspect = w.to_f / h.to_f - aspect.zero? ? nil : aspect + calculate_aspect(dar) end def aspect_from_sar - return nil unless sar - w, h = sar.split(":") - aspect = w.to_f / h.to_f - aspect.zero? ? nil : aspect + calculate_aspect(sar) + end + + def calculate_aspect(ratio) + return nil unless ratio + w, h = ratio.split(':') + return nil if w == '0' || h == '0' + @rotation.nil? || (@rotation == 180) ? (w.to_f / h.to_f) : (h.to_f / w.to_f) end def aspect_from_dimensions @@ -132,5 +230,26 @@ def fix_encoding(output) 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 + response = http.request_head(url.request_uri) + + case response + when Net::HTTPRedirection then + raise FFMPEG::HTTPTooManyRequests if limit == 0 + new_uri = url + URI(response['Location']) + + head(new_uri, limit - 1) + else + response + end + rescue SocketError, Errno::ECONNREFUSED => e + nil + end end end diff --git a/lib/ffmpeg/transcoder.rb b/lib/ffmpeg/transcoder.rb index 1d27f899..98064632 100644 --- a/lib/ffmpeg/transcoder.rb +++ b/lib/ffmpeg/transcoder.rb @@ -1,34 +1,42 @@ require 'open3' -require 'shellwords' module FFMPEG class Transcoder - @@timeout = 30 + attr_reader :command, :input - def self.timeout=(time) - @@timeout = time - end + @@timeout = 30 - def self.timeout - @@timeout + class << self + attr_accessor :timeout end - def initialize(movie, output_file, options = EncodingOptions.new, transcoder_options = {}) - @movie = movie + def initialize(input, output_file, options = EncodingOptions.new, transcoder_options = {}) + if input.is_a?(FFMPEG::Movie) + @movie = input + @input = input.path + end @output_file = output_file - if options.is_a?(String) || options.is_a?(EncodingOptions) + if options.is_a?(Array) || options.is_a?(EncodingOptions) @raw_options = options elsif options.is_a?(Hash) @raw_options = EncodingOptions.new(options) else - raise ArgumentError, "Unknown options format '#{options.class}', should be either EncodingOptions, Hash or String." + raise ArgumentError, "Unknown options format '#{options.class}', should be either EncodingOptions, Hash or Array." end @transcoder_options = transcoder_options @errors = [] apply_transcoder_options + + @input = @transcoder_options[:input] unless @transcoder_options[:input].nil? + + input_options = @transcoder_options[:input_options] || [] + iopts = [] + input_options.each { |k, v| iopts += ['-' + k.to_s, v] } + + @command = [FFMPEG.ffmpeg_binary, '-y', *iopts, '-i', @input, *@raw_options.to_a, @output_file] end def run(&block) @@ -42,7 +50,7 @@ def run(&block) 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.exist?(@output_file) @errors << "encoded file is invalid" and return false unless encoded.valid? true end @@ -51,14 +59,17 @@ def encoded @encoded ||= Movie.new(@output_file) end + def timeout + self.class.timeout + 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)}" - FFMPEG.logger.info("Running transcoding...\n#{@command}\n") + FFMPEG.logger.info("Running transcoding...\n#{command}\n") @output = "" - Open3.popen3(@command) do |stdin, stdout, stderr, wait_thr| + Open3.popen3(*command) do |_stdin, _stdout, stderr, wait_thr| begin yield(0.0) if block_given? next_line = Proc.new do |line| @@ -70,19 +81,22 @@ def transcode_movie else # better make sure it wont blow up in case of unexpected output time = 0.0 end - progress = time / @movie.duration - yield(progress) if block_given? + + if @movie + progress = time / @movie.duration + yield(progress) if block_given? + end end end - if @@timeout - stderr.each_with_timeout(wait_thr.pid, @@timeout, 'size=', &next_line) + if timeout + stderr.each_with_timeout(wait_thr.pid, timeout, 'size=', &next_line) else stderr.each('size=', &next_line) end rescue Timeout::Error => e - FFMPEG.logger.error "Process hung...\n@command\n#{@command}\nOutput\n#{@output}\n" + FFMPEG.logger.error "Process hung...\n@command\n#{command}\nOutput\n#{@output}\n" raise Error, "Process hung. Full output: #{@output}" end end @@ -91,10 +105,10 @@ def transcode_movie def validate_output_file(&block) if 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 #{input} to #{@output_file} succeeded\n" else errors = "Errors: #{@errors.join(", ")}. " - FFMPEG.logger.error "Failed encoding...\n#{@command}\n\n#{@output}\n#{errors}\n" + FFMPEG.logger.error "Failed encoding...\n#{command}\n\n#{@output}\n#{errors}\n" raise Error, "Failed encoding.#{errors}Full output: #{@output}" end end @@ -103,7 +117,7 @@ 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? + return if @movie.nil? || @movie.calculated_aspect_ratio.nil? case @transcoder_options[:preserve_aspect_ratio].to_s when "width" new_height = @raw_options.width / @movie.calculated_aspect_ratio diff --git a/lib/ffmpeg/version.rb b/lib/ffmpeg/version.rb index d7200442..8307d06c 100644 --- a/lib/ffmpeg/version.rb +++ b/lib/ffmpeg/version.rb @@ -1,3 +1,3 @@ module FFMPEG - VERSION = "1.0.0" + VERSION = '3.0.2' end diff --git a/lib/streamio-ffmpeg.rb b/lib/streamio-ffmpeg.rb index 4e00df99..83163784 100644 --- a/lib/streamio-ffmpeg.rb +++ b/lib/streamio-ffmpeg.rb @@ -35,14 +35,74 @@ def self.logger # # @param [String] path to the ffmpeg binary # @return [String] the path you set + # @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" + end @ffmpeg_binary = bin end # Get the path to the ffmpeg binary, defaulting to 'ffmpeg' # # @return [String] the path to the ffmpeg binary + # @raise Errno::ENOENT if the ffmpeg binary cannot be found def self.ffmpeg_binary - @ffmpeg_binary || 'ffmpeg' + @ffmpeg_binary || which('ffmpeg') end + + # Get the path to the ffprobe binary, defaulting to what is on ENV['PATH'] + # + # @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') + end + + # Set the path of the ffprobe binary. + # Can be useful if you need to specify a path such as /usr/local/bin/ffprobe + # + # @param [String] path to the ffprobe binary + # @return [String] the path you set + # @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" + end + @ffprobe_binary = bin + end + + # Get the maximum number of http redirect attempts + # + # @return [Integer] the maximum number of retries + def self.max_http_redirect_attempts + @max_http_redirect_attempts.nil? ? 10 : @max_http_redirect_attempts + end + + # Set the maximum number of http redirect attempts. + # + # @param [Integer] the maximum number of retries + # @return [Integer] the number of retries you set + # @raise Errno::ENOENT if the value is negative or not an Integer + def self.max_http_redirect_attempts=(v) + raise Errno::ENOENT, 'max_http_redirect_attempts must be an integer' if v && !v.is_a?(Integer) + raise Errno::ENOENT, 'max_http_redirect_attempts may not be negative' if v && v < 0 + @max_http_redirect_attempts = v + end + + # 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| + exts.each { |ext| + exe = File.join(path, "#{cmd}#{ext}") + return exe if File.executable? exe + } + end + raise Errno::ENOENT, "the #{cmd} binary could not be found in #{ENV['PATH']}" + end + end diff --git a/spec/ffmpeg/encoding_options_spec.rb b/spec/ffmpeg/encoding_options_spec.rb index 15c09bb7..a9676687 100644 --- a/spec/ffmpeg/encoding_options_spec.rb +++ b/spec/ffmpeg/encoding_options_spec.rb @@ -4,175 +4,203 @@ module FFMPEG describe EncodingOptions do describe "ffmpeg arguments conversion" do it "should convert video codec" do - EncodingOptions.new(video_codec: "libx264").to_s.should == "-vcodec libx264" + expect(EncodingOptions.new(video_codec: "libx264").to_a).to eq(%w(-vcodec libx264)) end it "should know the width from the resolution or be nil" do - EncodingOptions.new(resolution: "320x240").width.should == 320 - EncodingOptions.new.width.should be_nil + expect(EncodingOptions.new(resolution: "320x240").width).to eq(320) + expect(EncodingOptions.new.width).to be_nil end it "should know the height from the resolution or be nil" do - EncodingOptions.new(resolution: "320x240").height.should == 240 - EncodingOptions.new.height.should be_nil + expect(EncodingOptions.new(resolution: "320x240").height).to eq(240) + expect(EncodingOptions.new.height).to be_nil end it "should convert frame rate" do - EncodingOptions.new(frame_rate: 29.9).to_s.should == "-r 29.9" + expect(EncodingOptions.new(frame_rate: 29.9).to_a).to eq(%w(-r 29.9)) end it "should convert the resolution" do - EncodingOptions.new(resolution: "640x480").to_s.should include("-s 640x480") + expect(EncodingOptions.new(resolution: "640x480").to_a).to include("-s", "640x480") end it "should add calculated aspect ratio" do - EncodingOptions.new(resolution: "640x480").to_s.should include("-aspect 1.3333333") - EncodingOptions.new(resolution: "640x360").to_s.should include("-aspect 1.7777777777777") + expect(EncodingOptions.new(resolution: "640x480").to_a).to include("-aspect", "1.3333333333333333") + expect(EncodingOptions.new(resolution: "640x360").to_a).to include("-aspect", "1.7777777777777777") end it "should use specified aspect ratio if given" do - output = EncodingOptions.new(resolution: "640x480", aspect: 1.77777777777778).to_s - output.should include("-s 640x480") - output.should include("-aspect 1.77777777777778") + output = EncodingOptions.new(resolution: "640x480", aspect: 1.77777777777778).to_a + expect(output).to include("-s", "640x480") + expect(output).to include("-aspect", "1.77777777777778") end it "should convert video bitrate" do - EncodingOptions.new(video_bitrate: "600k").to_s.should == "-b:v 600k" + expect(EncodingOptions.new(video_bitrate: "600k").to_a).to eq(%w(-b:v 600k)) end it "should use k unit for video bitrate" do - EncodingOptions.new(video_bitrate: 600).to_s.should == "-b:v 600k" + expect(EncodingOptions.new(video_bitrate: 600).to_a).to eq(%w(-b:v 600k)) end it "should convert audio codec" do - EncodingOptions.new(audio_codec: "aac").to_s.should == "-acodec aac" + expect(EncodingOptions.new(audio_codec: "aac").to_a).to eq( %w(-acodec aac)) end it "should convert audio bitrate" do - EncodingOptions.new(audio_bitrate: "128k").to_s.should == "-b:a 128k" + expect(EncodingOptions.new(audio_bitrate: "128k").to_a).to eq(%w(-b:a 128k)) end it "should use k unit for audio bitrate" do - EncodingOptions.new(audio_bitrate: 128).to_s.should == "-b:a 128k" + expect(EncodingOptions.new(audio_bitrate: "128k").to_a).to eq(%w(-b:a 128k)) end it "should convert audio sample rate" do - EncodingOptions.new(audio_sample_rate: 44100).to_s.should == "-ar 44100" + expect(EncodingOptions.new(audio_sample_rate: 44100).to_a).to eq(%w(-ar 44100)) end it "should convert audio channels" do - EncodingOptions.new(audio_channels: 2).to_s.should == "-ac 2" + expect(EncodingOptions.new(audio_channels: 2).to_a).to eq(%w(-ac 2)) end it "should convert maximum video bitrate" do - EncodingOptions.new(video_max_bitrate: 600).to_s.should == "-maxrate 600k" + expect(EncodingOptions.new(video_max_bitrate: 600).to_a).to eq(%w(-maxrate 600k)) end - it "should convert mininimum video bitrate" do - EncodingOptions.new(video_min_bitrate: 600).to_s.should == "-minrate 600k" + it "should convert minimum video bitrate" do + expect(EncodingOptions.new(video_min_bitrate: 600).to_a).to eq(%w(-minrate 600k)) end it "should convert video bitrate tolerance" do - EncodingOptions.new(video_bitrate_tolerance: 100).to_s.should == "-bt 100k" + expect(EncodingOptions.new(video_bitrate_tolerance: 100).to_a).to eq(%w(-bt 100k)) end it "should convert buffer size" do - EncodingOptions.new(buffer_size: 2000).to_s.should == "-bufsize 2000k" + expect(EncodingOptions.new(buffer_size: 2000).to_a).to eq(%w(-bufsize 2000k)) end it "should convert threads" do - EncodingOptions.new(threads: 2).to_s.should == "-threads 2" + expect(EncodingOptions.new(threads: 2).to_a).to eq(%w(-threads 2)) end it "should convert duration" do - EncodingOptions.new(duration: 30).to_s.should == "-t 30" + expect(EncodingOptions.new(duration: 30).to_a).to eq(%w(-t 30)) + end + + it "should convert target" do + expect(EncodingOptions.new(target: 'ntsc-vcd').to_a).to eq(%w(-target ntsc-vcd)) end it "should convert keyframe interval" do - EncodingOptions.new(keyframe_interval: 60).to_s.should == "-g 60" + expect(EncodingOptions.new(keyframe_interval: 60).to_a).to eq(%w(-g 60)) end it "should convert video preset" do - EncodingOptions.new(video_preset: "max").to_s.should == "-vpre max" + expect(EncodingOptions.new(video_preset: "max").to_a).to eq(%w(-vpre max)) end it "should convert audio preset" do - EncodingOptions.new(audio_preset: "max").to_s.should == "-apre max" + expect(EncodingOptions.new(audio_preset: "max").to_a).to eq(%w(-apre max)) end it "should convert file preset" do - EncodingOptions.new(file_preset: "max.ffpreset").to_s.should == "-fpre max.ffpreset" + expect(EncodingOptions.new(file_preset: "max.ffpreset").to_a).to eq(%w(-fpre max.ffpreset)) end it "should specify seek time" do - EncodingOptions.new(seek_time: 1).to_s.should == "-ss 1" + expect(EncodingOptions.new(seek_time: 1).to_a).to eq(%w(-ss 1)) + end + + it "should specify default screenshot parameters" do + expect(EncodingOptions.new(screenshot: true).to_a).to eq(%w(-vframes 1 -f image2)) end - it "should specify screenshot parameters" do - EncodingOptions.new(screenshot: true).to_s.should == "-vframes 1 -f image2" + it 'should specify screenshot parameters when using -vframes' do + expect(EncodingOptions.new(screenshot: true, vframes: 123).to_a).to eq(%w(-f image2 -vframes 123)) end - it "should put the parameters in order of codecs, presets, others" do + it 'should specify screenshot parameters when using video quality -v:q' do + expect(EncodingOptions.new(screenshot: true, vframes: 123, quality: 3).to_a).to eq(%w(-f image2 -vframes 123 -q:v 3)) + end + + it 'should put the parameters in order of codecs, presets, others' do opts = Hash.new opts[:frame_rate] = 25 - opts[:video_codec] = "libx264" - opts[:video_preset] = "normal" + opts[:video_codec] = 'libx264' + opts[:video_preset] = 'normal' + opts[:watermark_filter] = { position: "RT", padding_x: 10, padding_y: 10} + opts[:watermark] = 'watermark.png' - converted = EncodingOptions.new(opts).to_s - converted.should == "-vcodec libx264 -vpre normal -r 25" + converted = EncodingOptions.new(opts).to_a + expect(converted).to eq(%w(-i watermark.png -filter_complex scale=,overlay=x=main_w-overlay_w-10:y=10 -vcodec libx264 -vpre normal -r 25)) end it "should convert a lot of them simultaneously" do - converted = EncodingOptions.new(video_codec: "libx264", audio_codec: "aac", video_bitrate: "1000k").to_s - converted.should match(/-acodec aac/) + converted = EncodingOptions.new(video_codec: "libx264", audio_codec: "aac", video_bitrate: "1000k").to_a + expect(converted).to include('-acodec', 'aac') end it "should ignore options with nil value" do - EncodingOptions.new(video_codec: "libx264", frame_rate: nil).to_s.should == "-vcodec libx264 " + expect(EncodingOptions.new(video_codec: "libx264", frame_rate: nil).to_a).to eq(%w(-vcodec libx264)) end it "should convert x264 vprofile" do - EncodingOptions.new(x264_vprofile: "high").to_s.should == "-vprofile high" + expect(EncodingOptions.new(x264_vprofile: "high").to_a).to eq(%w(-vprofile high)) end it "should convert x264 preset" do - EncodingOptions.new(x264_preset: "slow").to_s.should == "-preset slow" + expect(EncodingOptions.new(x264_preset: "slow").to_a).to eq(%w(-preset slow)) end it "should specify input watermark file" do - EncodingOptions.new(watermark: "watermark.png").to_s.should == "-i watermark.png" + expect(EncodingOptions.new(watermark: "watermark.png").to_a).to eq(%w(-i watermark.png)) end it "should specify watermark position at left top corner" do opts = Hash.new opts[:resolution] = "640x480" opts[:watermark_filter] = { position: "LT", padding_x: 10, padding_y: 10 } - converted = EncodingOptions.new(opts).to_s - converted.should include "-filter_complex 'scale=640x480,overlay=x=10:y=10'" + converted = EncodingOptions.new(opts).to_a + expect(converted).to include "-filter_complex", 'scale=640x480,overlay=x=10:y=10' end - it "should specify watermark position at right top corner" do - opts = Hash.new - opts[:resolution] = "640x480" - opts[:watermark_filter] = { position: "RT", padding_x: 10, padding_y: 10 } - converted = EncodingOptions.new(opts).to_s - converted.should include "-filter_complex 'scale=640x480,overlay=x=main_w-overlay_w-10:y=10'" + it 'should specify watermark position at right top corner' do + opts = { + resolution: '640x480', + watermark_filter: { position: 'RT', padding_x: 10, padding_y: 10 } + } + converted = EncodingOptions.new(opts).to_a + expect(converted).to include "-filter_complex", 'scale=640x480,overlay=x=main_w-overlay_w-10:y=10' end - it "should specify watermark position at left bottom corner" do - opts = Hash.new - opts[:resolution] = "640x480" - opts[:watermark_filter] = { position: "LB", padding_x: 10, padding_y: 10 } - converted = EncodingOptions.new(opts).to_s - converted.should include "-filter_complex 'scale=640x480,overlay=x=10:y=main_h-overlay_h-10'" + it 'should specify watermark position at left bottom corner' do + opts = { + resolution: '640x480', + watermark_filter: { position: 'LB', padding_x: 10, padding_y: 10 } + } + converted = EncodingOptions.new(opts).to_a + expect(converted).to include "-filter_complex", 'scale=640x480,overlay=x=10:y=main_h-overlay_h-10' end it "should specify watermark position at left bottom corner" do - opts = Hash.new - opts[:resolution] = "640x480" - opts[:watermark_filter] = { position: "RB", padding_x: 10, padding_y: 10 } - converted = EncodingOptions.new(opts).to_s - converted.should include "overlay=x=main_w-overlay_w-10:y=main_h-overlay_h-10'" + opts = { + resolution: '640x480', + watermark_filter: { position: 'RB', padding_x: 10, padding_y: 10 } + } + converted = EncodingOptions.new(opts).to_a + expect(converted.find{|str| str =~ /overlay/ }).to include "overlay=x=main_w-overlay_w-10:y=main_h-overlay_h-10" + end + + context 'for custom options' do + it 'should not allow custom options as String' do + expect { EncodingOptions.new({ custom: '-map 0:0 -map 0:1' }).to_a }.to raise_error(ArgumentError) + end + + it 'should correctly include custom options' do + converted = EncodingOptions.new({ custom: %w(-map 0:0 -map 0:1) }).to_a + expect(converted).to eq(['-map', '0:0', '-map', '0:1']) + end end end end diff --git a/spec/ffmpeg/movie_spec.rb b/spec/ffmpeg/movie_spec.rb index 28c33f91..19281a3e 100644 --- a/spec/ffmpeg/movie_spec.rb +++ b/spec/ffmpeg/movie_spec.rb @@ -1,340 +1,575 @@ require 'spec_helper.rb' +require 'net/http' +require 'webrick' module FFMPEG describe Movie do describe "initializing" do + let(:fake_output) { StringIO.new(File.read("#{fixture_path}/outputs/#{fixture_file}")) } + let(:movie) do + allow(Open3).to receive(:popen3).and_yield(nil, nil, fake_output) + Movie.new(__FILE__) + end + context "given a non existing file" do + let(:movie) { Movie.new('i_dont_exist') } + it "should throw ArgumentError" do - expect { Movie.new("i_dont_exist") }.to raise_error(Errno::ENOENT, /does not exist/) + expect { movie }.to raise_error(Errno::ENOENT, /does not exist/) end end context "given a file containing a single quotation mark in the filename" do - before(:all) do - @movie = Movie.new("#{fixture_path}/movies/awesome'movie.mov") - end + let(:movie) { Movie.new("#{fixture_path}/movies/awesome'movie.mov") } it "should run ffmpeg successfully" do - @movie.duration.should == 7.56 - @movie.frame_rate.should == 16.75 + expect(movie.duration).to be_within(0.01).of(7.56) + expect(movie.frame_rate).to be_within(0.01).of(16.75) end end - context "given a non movie file" do - before(:all) do - @movie = Movie.new(__FILE__) + context "given a URL" do + before(:context) { start_web_server } + after(:context) { stop_web_server } + context "that is correct" do + + let(:movie) { Movie.new("http://127.0.0.1:8000/awesome%20movie.mov") } + + it "should be valid" do + expect(movie).to be_valid + end + + it "should know the file size" do + expect(movie.size).to eq(455546) + end + + it "should remember the movie URL" do + expect(movie.path).to eq("http://127.0.0.1:8000/awesome%20movie.mov") + end + + it "should be marked as remote" do + expect(movie.remote?).to be_truthy + end + + context 'with a query string' do + # We're mocking this to fail on the same URL with the query string added. + # This means that we're passing the query string through using .request_uri + # rather than .path + + it 'should not be found' do + expect { Movie.new('http://127.0.0.1:8000/awesome%20movie.mov?fail=1') }.to raise_error(Errno::ENOENT) + end + end + end + context "that does not exist" do + it "should raise an exception" do + expect { Movie.new("http://127.0.0.1:8000/awesome%20movie_missing.mov") }.to raise_error(Errno::ENOENT) + end + end + context 'that redirects' do + context 'to a remote uri' do + let(:movie) { Movie.new('http://www.redirect-example.com/moved_movie.mov') } + + it "should know the file size" do + expect(movie.size).to eq(455_546) + end + end + context 'to a relative uri' do + let(:movie) { Movie.new('http://127.0.0.1:8000/deep_path/awesome%20movie.mov') } + + it 'should know the file size' do + expect(movie.size).to eq(455_546) + end + end + context 'to a relative uri respecting redirect limits' do + before { FFMPEG.max_http_redirect_attempts = 0 } + after { FFMPEG.max_http_redirect_attempts = nil } + + it 'raise FFMPEG::HTTPTooManyRequests' do + expect { Movie.new('http://127.0.0.1:8000/deep_path/awesome%20movie.mov') }.to raise_error(FFMPEG::HTTPTooManyRequests) + end + end + + context 'to a relative uri with too many redirects' do + it 'should know the file size' do + expect { Movie.new('http://www.toomany-redirects-example.com/moved_movie.mov') }.to raise_error(FFMPEG::HTTPTooManyRequests) + end + end end + end + + context "given a non movie file" do + let(:movie) { Movie.new(__FILE__) } it "should not be valid" do - @movie.should_not be_valid + expect(movie).to_not be_valid end it "should have a duration of 0" do - @movie.duration.should == 0 + expect(movie.duration).to eq(0) end it "should have nil height" do - @movie.height.should be_nil + expect(movie.height).to be_nil end it "should have nil width" do - @movie.width.should be_nil + expect(movie.width).to be_nil end it "should have nil frame_rate" do - @movie.frame_rate.should be_nil + expect(movie.frame_rate).to be_nil end end context "given an empty flv file (could not find codec parameters)" do - before(:all) do - @movie = Movie.new("#{fixture_path}/movies/empty.flv") - end + let(:movie) { Movie.new("#{fixture_path}/movies/empty.flv") } it "should not be valid" do - @movie.should_not be_valid + expect(movie).to_not be_valid end end - context "given a broken mp4 file" do - before(:all) do - @movie = Movie.new("#{fixture_path}/movies/broken.mp4") + context "given an ios mobile video filmed without rotation" do + let(:movie) { Movie.new("#{fixture_path}/movies/ios_rotate0.mov") } + + it "should have correct rotation detected" do + expect(movie.rotation).to eq(nil) + end + it "should have untouched width and height" do + expect(movie.width).to eq(1920) + expect(movie.height).to eq(1080) end + end - it "should not be valid" do - @movie.should_not be_valid + context "given an ios mobile video filmed with rotation=90" do + let(:movie) { Movie.new("#{fixture_path}/movies/ios_rotate90.mov") } + + it "should have correct rotation detected" do + expect(movie.rotation).to eq(90) end - it "should have nil calculated_aspect_ratio" do - @movie.calculated_aspect_ratio.should be_nil + it "should have switched width and height" do + expect(movie.width).to eq(1080) + expect(movie.height).to eq(1920) end end - context "given a weird aspect ratio file" do - before(:all) do - @movie = Movie.new("#{fixture_path}/movies/weird_aspect.small.mpg") - end + context "given an ios mobile video filmed with rotation=180" do + let(:movie) { Movie.new("#{fixture_path}/movies/ios_rotate180.mov") } - it "should parse the DAR" do - @movie.dar.should == "704:405" + it "should have correct rotation detected" do + expect(movie.rotation).to eq(180) end + it "should have untouched width and height" do + expect(movie.width).to eq(1920) + expect(movie.height).to eq(1080) + end + end - it "should have correct calculated_aspect_ratio" do - @movie.calculated_aspect_ratio.to_s[0..14].should == "1.7382716049382" # substringed to be 1.9 compatible + context "given an ios mobile video filmed with rotation=270" do + let(:movie) { Movie.new("#{fixture_path}/movies/ios_rotate270.mov") } + + it "should have correct rotation detected" do + expect(movie.rotation).to eq(270) + end + it "should have switched width and height" do + expect(movie.width).to eq(1080) + expect(movie.height).to eq(1920) end end - context "given an impossible DAR" do - before(:all) do - fake_output = StringIO.new(File.read("#{fixture_path}/outputs/file_with_weird_dar.txt")) - Open3.stub(:popen3).and_yield(nil,nil,fake_output) - @movie = Movie.new(__FILE__) + context "given an ios9 mov file (with superfluous data streams)" do + let(:movie) { Movie.new("#{fixture_path}/movies/ios9.mov") } + + it "should be valid" do + expect(movie).to be_valid end + end - it "should parse the DAR" do - @movie.dar.should == "0:1" + context "given a broken mp4 file" do + let(:movie) { Movie.new("#{fixture_path}/movies/broken.mp4") } + + it "should not be valid" do + expect(movie).to_not be_valid end - it "should calulate using width and height instead" do - @movie.calculated_aspect_ratio.to_s[0..14].should == "1.7777777777777" # substringed to be 1.9 compatible + it "should have nil calculated_aspect_ratio" do + expect(movie.calculated_aspect_ratio).to be_nil end end - context "given a weird storage/pixel aspect ratio file" do - before(:all) do - @movie = Movie.new("#{fixture_path}/movies/weird_aspect.small.mpg") - end + context "given a file with data streams" do + let(:movie) { Movie.new("#{fixture_path}/movies/file_with_data_streams.mp4") } - it "should parse the SAR" do - @movie.sar.should == "64:45" + it "should be valid" do + expect(movie).to be_valid end + end - it "should have correct calculated_pixel_aspect_ratio" do - @movie.calculated_pixel_aspect_ratio.to_s[0..14].should == "1.4222222222222" # substringed to be 1.9 compatible + context "given a file named with URL characters" do + let(:movie) { Movie.new("#{fixture_path}/movies/file+with+data&streams=works?.mp4") } + + it "should be valid" do + expect(movie).to be_valid end end - context "given a colorspace with parenthesis but no commas such as yuv420p(tv)" do - before(:all) do - fake_output = StringIO.new(File.read("#{fixture_path}/outputs/file_with_colorspace_with_parenthesis_but_no_comma.txt")) - Open3.stub(:popen3).and_yield(nil,nil,fake_output) - @movie = Movie.new(__FILE__) + context "given a weird aspect ratio file" do + let(:movie) { Movie.new("#{fixture_path}/movies/weird_aspect.small.mpg") } + + it "should parse the DAR" do + expect(movie.dar).to eq("704:405") end - it "should have correct video stream" do - @movie.colorspace.should == "yuv420p(tv)" + it "should have correct calculated_aspect_ratio" do + expect(movie.calculated_aspect_ratio.to_s[0..14]).to eq("1.7382716049382") # substringed to be 1.9 compatible end end - context "given an impossible SAR" do - before(:all) do - fake_output = StringIO.new(File.read("#{fixture_path}/outputs/file_with_weird_sar.txt")) - Open3.stub(:popen3).and_yield(nil,nil,fake_output) - @movie = Movie.new(__FILE__) + describe 'Special cases: ' do + let(:movie) do + allow(Open3).to receive(:popen3).and_yield(nil,fake_output,nil) + Movie.new(__FILE__) end - it "should parse the SAR" do - @movie.sar.should == "0:1" - end + context "given a file with bad JSON" do + let(:fixture_file) { 'file_with_bad_json.txt' } - it "should using square SAR, 1.0 instead" do - @movie.calculated_pixel_aspect_ratio.to_s[0..14].should == "1" # substringed to be 1.9 compatible + it 'should raise an exception' do + expect { movie }.to raise_error RuntimeError, /Could not parse output from FFProbe/ + end end - end - context "given a file with ISO-8859-1 characters in output" do - it "should not crash" do - fake_output = StringIO.new(File.read("#{fixture_path}/outputs/file_with_iso-8859-1.txt")) - Open3.stub(:popen3).and_yield(nil, nil, fake_output) - expect { Movie.new(__FILE__) }.to_not raise_error + context "given an impossible DAR" do + let(:fixture_file) { 'file_with_weird_dar.txt' } + + it "should parse the DAR" do + expect(movie.dar).to eq('0:1') + end + + it "should calculate using width and height instead" do + expect(movie.calculated_aspect_ratio.to_s[0..14]).to eq("1.7777777777777") # substringed to be 1.9 compatible + end + + context 'when width/height is flipped' do + before { movie.instance_variable_set :@rotation, 90 } + it "should calculate using width and height instead" do + expect(movie.calculated_aspect_ratio).to eq(0.5625) + end + end end - end - context "given a file with 5.1 audio" do - before(:all) do - fake_output = StringIO.new(File.read("#{fixture_path}/outputs/file_with_surround_sound.txt")) - Open3.stub(:popen3).and_yield(nil, nil, fake_output) - @movie = Movie.new(__FILE__) + context 'given an impossible SAR' do + let(:fixture_file) { 'file_with_weird_sar.txt' } + + it 'should parse the SAR' do + expect(movie.sar).to eq('0:1') + end + + it 'should using square SAR, 1.0 instead' do + expect(movie.calculated_pixel_aspect_ratio.to_s[0..14]).to eq('1') # substringed to be 1.9 compatible + end + + context 'when width/height is flipped' do + before { movie.instance_variable_set :@rotation, 90 } + it 'should using square SAR, 1.0 instead' do + expect(movie.calculated_pixel_aspect_ratio).to eq(1) + end + end end - it "should have 6 audio channels" do - @movie.audio_channels.should == 6 + context "given a file with ISO-8859-1 characters in output" do + let(:fixture_file) { 'file_with_iso-8859-1.txt' } + + it "should not crash" do + expect { movie }.to_not raise_error + end end - end - context "given a file with no audio" do - before(:all) do - fake_output = StringIO.new(File.read("#{fixture_path}/outputs/file_with_no_audio.txt")) - Open3.stub(:popen3).and_yield(nil, nil, fake_output) - @movie = Movie.new(__FILE__) + context "given a file with 5.1 audio" do + let(:fixture_file) { 'file_with_surround_sound.txt' } + + it "should have 6 audio channels" do + expect(movie.audio_channels).to eq(6) + end end - it "should have nil audio channels" do - @movie.audio_channels.should == nil + context "given a file with no audio" do + let(:fixture_file) { 'file_with_no_audio.txt' } + + it "should have nil audio channels" do + expect(movie.audio_channels).to eq(nil) + end end - end - context "given a file with non supported audio" do - before(:all) do - fake_output = StringIO.new(File.read("#{fixture_path}/outputs/file_with_non_supported_audio.txt")) - Open3.stub(:popen3).and_yield(nil, nil, fake_output) - @movie = Movie.new(__FILE__) + context "given a file with non supported audio" do + let(:fixture_file) { 'file_with_non_supported_audio_stdout.txt' } + let(:movie) do + fake_stderr = StringIO.new(File.read("#{fixture_path}/outputs/file_with_non_supported_audio_stderr.txt")) + allow(Open3).to receive(:popen3).and_yield(nil,fake_output,fake_stderr) + Movie.new(__FILE__) + end + + it "should be valid" do + expect(movie).to be_valid + end end - it "should not be valid" do - @movie.should_not be_valid + context "given a file with non supported audio and video" do + let(:fixture_file) { 'file_with_non_supported_audio_and_video_stdout.txt' } + let(:movie) do + fake_stderr = StringIO.new(File.read("#{fixture_path}/outputs/file_with_non_supported_audio_and_video_stderr.txt")) + allow(Open3).to receive(:popen3).and_yield(nil, fake_output, fake_stderr) + Movie.new(__FILE__) + end + + it "should not be valid" do + expect(movie).to_not be_valid + end end + end - context "given a file with complex colorspace and decimal fps" do - before(:all) do - fake_output = StringIO.new(File.read("#{fixture_path}/outputs/file_with_complex_colorspace_and_decimal_fps.txt")) - Open3.stub(:popen3).and_yield(nil, nil, fake_output) - @movie = Movie.new(__FILE__) - end + context 'given a file with improperly formatted creation_time' do + let(:movie) { Movie.new("#{fixture_path}/sounds/bad_metadata_creation_time.wav") } - it "should know the framerate" do - @movie.frame_rate.should == 23.98 + it 'should set creation_time to nil' do + expect(movie.creation_time).to be_nil end + end - it "should know the colorspace" do - @movie.colorspace.should == "yuv420p(tv, bt709)" + context "given a weird storage/pixel aspect ratio file" do + let(:movie) { Movie.new("#{fixture_path}/movies/weird_aspect.small.mpg") } + + it "should parse the SAR" do + expect(movie.sar).to eq("64:45") end - it "should know the width and height" do - @movie.width.should == 960 - @movie.height.should == 540 + it "should have correct calculated_pixel_aspect_ratio" do + expect(movie.calculated_pixel_aspect_ratio.to_s[0..14]).to eq("1.4222222222222") # substringed to be 1.9 compatible end end context "given an awesome movie file" do - before(:all) do - @movie = Movie.new("#{fixture_path}/movies/awesome movie.mov") + let(:movie) { Movie.new("#{fixture_path}/movies/awesome movie.mov") } + + it 'exposes the format tags' do + expect(movie.format_tags.keys).to include(:major_brand, :minor_version, :compatible_brands, :creation_time) + end + + it 'exposes the metadata' do + expect(movie.metadata.keys).to include(:streams, :format) end it "should remember the movie path" do - @movie.path.should == "#{fixture_path}/movies/awesome movie.mov" + expect(movie.path).to eq("#{fixture_path}/movies/awesome movie.mov") + end + + it "should be marked as local" do + expect(movie.local?).to be_truthy end it "should parse duration to number of seconds" do - @movie.duration.should == 7.56 + expect(movie.duration).to be_within(0.01).of(7.56) end it "should parse the bitrate" do - @movie.bitrate.should == 481 + expect(movie.bitrate).to eq(481846) end it "should return nil rotation when no rotation exists" do - @movie.rotation.should == nil + expect(movie.rotation).to eq(nil) end it "should parse the creation_time" do - @movie.creation_time.should == Time.parse("2010-02-05 16:05:04") + expect(movie.creation_time).to eq(Time.parse("2010-02-05 16:05:04")) end it "should parse video stream information" do - @movie.video_stream.should == "h264 (Main) (avc1 / 0x31637661), yuv420p, 640x480 [SAR 1:1 DAR 4:3], 371 kb/s, 16.75 fps, 600 tbr, 600 tbn, 1200 tbc" + expect(movie.video_stream).to eq("h264 (Main) (avc1 / 0x31637661), yuv420p, 640x480 [SAR 1:1 DAR 4:3]") end it "should know the video codec" do - @movie.video_codec.should =~ /h264/ + expect(movie.video_codec).to match(/h264/) end it "should know the colorspace" do - @movie.colorspace.should == "yuv420p" + expect(movie.colorspace).to eq("yuv420p") end it "should know the resolution" do - @movie.resolution.should == "640x480" + expect(movie.resolution).to eq("640x480") end it "should know the video bitrate" do - @movie.video_bitrate.should == 371 + expect(movie.video_bitrate).to eq(371185) end it "should know the width and height" do - @movie.width.should == 640 - @movie.height.should == 480 + expect(movie.width).to eq(640) + expect(movie.height).to eq(480) end it "should know the framerate" do - @movie.frame_rate.should == 16.75 + expect(movie.frame_rate).to be_within(0.01).of(16.75) end it "should parse audio stream information" do - @movie.audio_stream.should == "aac (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 75 kb/s" + expect(movie.audio_stream).to eq("aac (mp4a / 0x6134706d), 44100 Hz, stereo, fltp, 75832 bit/s") end it "should know the audio codec" do - @movie.audio_codec.should =~ /aac/ + expect(movie.audio_codec).to match(/aac/) end it "should know the sample rate" do - @movie.audio_sample_rate.should == 44100 + expect(movie.audio_sample_rate).to eq(44100) end it "should know the number of audio channels" do - @movie.audio_channels.should == 2 + expect(movie.audio_channels).to eq(2) end it "should know the audio bitrate" do - @movie.audio_bitrate.should == 75 + expect(movie.audio_bitrate).to eq(75832) end - it "should should be valid" do - @movie.should be_valid + it "should be valid" do + expect(movie).to be_valid end it "should calculate the aspect ratio" do - @movie.calculated_aspect_ratio.to_s[0..14].should == "1.3333333333333" # substringed to be 1.9 compatible + expect(movie.calculated_aspect_ratio.to_s[0..14]).to eq("1.3333333333333") # substringed to be 1.9 compatible end it "should know the file size" do - @movie.size.should == 455546 + expect(movie.size).to eq(455546) end it "should know the container" do - @movie.container.should == "mov,mp4,m4a,3gp,3g2,mj2" + expect(movie.container).to eq("mov,mp4,m4a,3gp,3g2,mj2") + end + end + + context "given a movie file with 2 audio streams" do + let(:movie) { Movie.new("#{fixture_path}/movies/multi_audio_movie.mp4") } + + it "should identify both audio streams" do + expect(movie.audio_streams.length).to eq(2) + end + + it "should assign audio_channels to the properties of the first stream" do + audio_channels = movie.audio_streams[0][:channels] + expect(movie.audio_channels).to eq audio_channels + end + + it "should assign audio_codec to the properties of the first stream" do + audio_codec = movie.audio_streams[0][:codec_name] + expect(movie.audio_codec).to eq audio_codec + end + + it "should assign audio_bitrate to the properties of the first stream" do + audio_bitrate = movie.audio_streams[0][:bitrate] + expect(movie.audio_bitrate).to eq audio_bitrate + end + + it "should assign audio_channel_layout to the properties of the first stream" do + audio_channel_layout = movie.audio_streams[0][:channel_layout] + expect(movie.audio_channel_layout).to eq audio_channel_layout + end + + it "should assign audio_tags to the properties of the first stream" do + audio_tags = movie.audio_streams[0][:tags] + expect(movie.audio_tags).to eq audio_tags + end + + it "should assign audio_stream to the properties of the first stream" do + stream_overview = movie.audio_streams[0][:overview] + expect(movie.audio_stream).to eq stream_overview end end end context "given a rotated movie file" do - before(:all) do - @movie = Movie.new("#{fixture_path}/movies/sideways movie.mov") - end + let(:movie) { Movie.new("#{fixture_path}/movies/sideways movie.mov") } it "should parse the rotation" do - @movie.rotation.should == 90 + expect(movie.rotation).to eq(90) end end describe "transcode" do + let(:movie) { Movie.new("#{fixture_path}/movies/awesome movie.mov")} + it "should run the transcoder" do - movie = Movie.new("#{fixture_path}/movies/awesome movie.mov") transcoder_double = double(Transcoder) - Transcoder.should_receive(:new). + expect(Transcoder).to receive(:new). with(movie, "#{tmp_path}/awesome.flv", {custom: "-vcodec libx264"}, preserve_aspect_ratio: :width). and_return(transcoder_double) - transcoder_double.should_receive(:run) + expect(transcoder_double).to receive(:run) + + movie.transcode("#{tmp_path}/awesome.flv", {custom: "-vcodec libx264"}, preserve_aspect_ratio: :width) + end + end + + describe "transcode" do + let(:movie) { Movie.new("#{fixture_path}/movies/awesome movie.mov")} + + it "should run the transcoder" do + + transcoder_double = double(Transcoder) + expect(Transcoder).to receive(:new). + with(movie, "#{tmp_path}/awesome.flv", {custom: "-vcodec libx264"}, preserve_aspect_ratio: :width). + and_return(transcoder_double) + expect(transcoder_double).to receive(:run) movie.transcode("#{tmp_path}/awesome.flv", {custom: "-vcodec libx264"}, preserve_aspect_ratio: :width) end end + describe 'transcode sound only' do + let(:movie) { Movie.new("#{fixture_path}/sounds/hello.wav")} + + it "should run the transcoder" do + + transcoder_double = double(Transcoder) + expect(Transcoder).to receive(:new). + with(movie, "#{tmp_path}/hello.mp3", {audio_codec: 'libmp3lame', custom: %w(-qscale:a 2)}, {}). + and_return(transcoder_double) + expect(transcoder_double).to receive(:run) + + movie.transcode("#{tmp_path}/hello.mp3", {audio_codec: 'libmp3lame', custom: %w(-qscale:a 2)}, {}) + end + end + describe "screenshot" do + let(:movie) { Movie.new("#{fixture_path}/movies/awesome movie.mov")} + it "should run the transcoder with screenshot option" do - movie = Movie.new("#{fixture_path}/movies/awesome movie.mov") transcoder_double = double(Transcoder) - Transcoder.should_receive(:new). + expect(Transcoder).to receive(:new). with(movie, "#{tmp_path}/awesome.jpg", {seek_time: 2, dimensions: "640x480", screenshot: true}, preserve_aspect_ratio: :width). and_return(transcoder_double) - transcoder_double.should_receive(:run) + expect(transcoder_double).to receive(:run) movie.screenshot("#{tmp_path}/awesome.jpg", {seek_time: 2, dimensions: "640x480"}, preserve_aspect_ratio: :width) end + + context 'with wildcard output filename' do + it 'should create multiple screenshots' do + + 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). + and_return(transcoder_double) + expect(transcoder_double).to receive(:run) + + movie.screenshot("#{tmp_path}/awesome_%d.jpg", {seek_time: 2, dimensions: '640x480', vframes: 20}, preserve_aspect_ratio: :width, validate: false) + end + end end end end diff --git a/spec/ffmpeg/transcoder_spec.rb b/spec/ffmpeg/transcoder_spec.rb index 82f7fa3a..6d92fcd3 100644 --- a/spec/ffmpeg/transcoder_spec.rb +++ b/spec/ffmpeg/transcoder_spec.rb @@ -15,19 +15,19 @@ module FFMPEG expect { Transcoder.new(movie, output_path, video_codec: "libx264") }.not_to raise_error end - it "should accept String as options" do - expect { Transcoder.new(movie, output_path, "-vcodec libx264") }.not_to raise_error + it 'should accept Array as options' do + expect { Transcoder.new(movie, output_path, %w(-vcodec libx264)) }.not_to raise_error end it "should not accept anything else as options" do - expect { Transcoder.new(movie, output_path, ["array?"]) }.to raise_error(ArgumentError, /Unknown options format/) + expect { Transcoder.new(movie, output_path, "string?") }.to raise_error(ArgumentError, /Unknown options format/) end end describe "transcoding" do context 'with default transcoder_options' do before do - FFMPEG.logger.should_receive(:info).at_least(:once) + expect(FFMPEG.logger).to receive(:info).at_least(:once) end context "when ffmpeg freezes" do @@ -40,7 +40,7 @@ module FFMPEG end it "should fail when the timeout is exceeded" do - FFMPEG.logger.should_receive(:error) + expect(FFMPEG.logger).to receive(:error) transcoder = Transcoder.new(movie, "#{tmp_path}/timeout.mp4") expect { transcoder.run }.to raise_error(FFMPEG::Error, /Process hung/) end @@ -57,9 +57,9 @@ module FFMPEG Transcoder.timeout = false end - it "should still work" do - encoded = Transcoder.new(movie, "#{tmp_path}/awesome.mpg").run - encoded.resolution.should == "640x480" + it 'should still work with (NTSC target)' do + encoded = Transcoder.new(movie, "#{tmp_path}/awesome.mpg", target: 'ntsc-vcd').run + expect(encoded.resolution).to eq('352x240') end after { Transcoder.timeout = @original_timeout } @@ -71,27 +71,42 @@ module FFMPEG transcoder = Transcoder.new(movie, "#{tmp_path}/awesome.flv") progress_updates = [] transcoder.run { |progress| progress_updates << progress } - transcoder.encoded.should be_valid - progress_updates.should include(0.0, 1.0) - progress_updates.length.should >= 3 - File.exists?("#{tmp_path}/awesome.flv").should be_true + 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 end it "should transcode the movie with EncodingOptions" do FileUtils.rm_f "#{tmp_path}/optionalized.mp4" options = {video_codec: "libx264", frame_rate: 10, resolution: "320x240", video_bitrate: 300, - audio_codec: "libfaac", audio_bitrate: 32, audio_sample_rate: 22050, audio_channels: 1} + audio_codec: "libmp3lame", audio_bitrate: 32, audio_sample_rate: 22050, audio_channels: 1} encoded = Transcoder.new(movie, "#{tmp_path}/optionalized.mp4", options).run - encoded.video_bitrate.should be_within(90).of(300) - encoded.video_codec.should =~ /h264/ - encoded.resolution.should == "320x240" - encoded.frame_rate.should == 10.0 - encoded.audio_bitrate.should be_within(2).of(32) - encoded.audio_codec.should =~ /aac/ - encoded.audio_sample_rate.should == 22050 - encoded.audio_channels.should == 1 + expect(encoded.video_bitrate).to be_within(90000).of(300000) + expect(encoded.video_codec).to match(/h264/) + expect(encoded.resolution).to eq("320x240") + expect(encoded.frame_rate).to eq(10.0) + expect(encoded.audio_bitrate).to be_within(2000).of(32000) + expect(encoded.audio_codec).to match(/mp3/) + expect(encoded.audio_sample_rate).to eq(22050) + expect(encoded.audio_channels).to eq(1) + end + + context 'audio only' do + let(:sound) { Movie.new("#{fixture_path}/sounds/hello.wav") } + it 'should transcode without video' do + FileUtils.rm_f "#{tmp_path}/hello.mp3" + + options = { audio_codec: "libmp3lame", custom: %w(-qscale:a 2)} + + encoded = Transcoder.new(sound, "#{tmp_path}/hello.mp3", options).run + expect(encoded.video_codec).to be_nil + expect(encoded.audio_codec).to match(/mp3/) + expect(encoded.audio_sample_rate).to eq(44100) + expect(encoded.audio_channels).to eq(1) + end end context "with aspect ratio preservation" do @@ -104,39 +119,39 @@ module FFMPEG special_options = {preserve_aspect_ratio: :width} encoded = Transcoder.new(@movie, "#{tmp_path}/preserved_aspect.mp4", @options, special_options).run - encoded.resolution.should == "320x180" + expect(encoded.resolution).to eq("320x180") end it "should work on height" do special_options = {preserve_aspect_ratio: :height} encoded = Transcoder.new(@movie, "#{tmp_path}/preserved_aspect.mp4", @options, special_options).run - encoded.resolution.should == "426x240" + expect(encoded.resolution).to eq("426x240") end it "should not be used if original resolution is undeterminable" do - @movie.should_receive(:calculated_aspect_ratio).and_return(nil) + expect(@movie).to receive(:calculated_aspect_ratio).and_return(nil) special_options = {preserve_aspect_ratio: :height} encoded = Transcoder.new(@movie, "#{tmp_path}/preserved_aspect.mp4", @options, special_options).run - encoded.resolution.should == "320x240" + expect(encoded.resolution).to eq("320x240") end it "should round to resolutions divisible by 2" do - @movie.should_receive(:calculated_aspect_ratio).at_least(:once).and_return(1.234) + expect(@movie).to receive(:calculated_aspect_ratio).at_least(:once).and_return(1.234) special_options = {preserve_aspect_ratio: :width} encoded = Transcoder.new(@movie, "#{tmp_path}/preserved_aspect.mp4", @options, special_options).run - encoded.resolution.should == "320x260" # 320 / 1.234 should at first be rounded to 259 + expect(encoded.resolution).to eq("320x260") # 320 / 1.234 should at first be rounded to 259 end end 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", "-s 300x200 -ac 2").run - encoded.resolution.should == "300x200" - encoded.audio_channels.should == 2 + encoded = Transcoder.new(movie, "#{tmp_path}/string_optionalized.flv", %w(-s 300x200 -ac 2)).run + expect(encoded.resolution).to eq("300x200") + expect(encoded.audio_channels).to eq(2) end it "should transcode the movie which name include single quotation mark" do @@ -153,10 +168,14 @@ module FFMPEG expect { Transcoder.new(movie, "#{tmp_path}/output with 'quote.flv").run }.not_to raise_error end - pending "should not crash on ISO-8859-1 characters (dont know how to spec this)" + 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 + end it "should fail when given an invalid movie" do - FFMPEG.logger.should_receive(:error) + expect(FFMPEG.logger).to receive(:error) movie = Movie.new(__FILE__) transcoder = Transcoder.new(movie, "#{tmp_path}/fail.flv") expect { transcoder.run }.to raise_error(FFMPEG::Error, /no output file created/) @@ -165,24 +184,49 @@ module FFMPEG it "should encode to the specified duration if given" do encoded = Transcoder.new(movie, "#{tmp_path}/durationalized.mp4", duration: 2).run - encoded.duration.should >= 1.8 - encoded.duration.should <= 2.2 + expect(encoded.duration).to be >= 1.8 + expect(encoded.duration).to be <= 2.2 + end + + context "with remote URL as input" do + before(:context) { start_web_server } + after(:context) { stop_web_server } + + 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 + end 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 - encoded.resolution.should == "640x480" + expect(encoded.resolution).to eq("640x480") end it "should transcode absolute resolution if specified" do encoded = Transcoder.new(movie, "#{tmp_path}/image.bmp", screenshot: true, seek_time: 3, resolution: '400x200').run - encoded.resolution.should == "400x200" + expect(encoded.resolution).to eq("400x200") end it "should be able to preserve aspect ratio" do encoded = Transcoder.new(movie, "#{tmp_path}/image.png", {screenshot: true, seek_time: 4, resolution: '320x500'}, preserve_aspect_ratio: :width).run - encoded.resolution.should == "320x240" + expect(encoded.resolution).to eq("320x240") + end + + describe 'for multiple screenshots' do + context 'with output file validation' do + it 'should fail' do + expect { Transcoder.new(movie, "#{tmp_path}/invalid_%d.png", {screenshot: true, seek_time: 4, resolution: '320x500'}, preserve_aspect_ratio: :width).run }.to raise_error(FFMPEG::Error, /Failed encoding/) + end + end + context 'without output file validation' do + it 'should create sequential screenshots' do + Transcoder.new(movie, "#{tmp_path}/screenshots_%d.png", {screenshot: true, seek_time: 4, resolution: '320x500'}, preserve_aspect_ratio: :width, validate: false).run + expect(Dir[File.join(tmp_path, 'screenshots_*.png')].count { |file| File.file?(file) }).to eq(1) + end + end end end @@ -195,9 +239,9 @@ module FFMPEG FFMPEG.ffmpeg_binary = "#{fixture_path}/bin/ffmpeg-audio-only" end - it "should not fail when the timeout is exceeded" do + it 'should fail when the timeout is exceeded' do transcoder = Transcoder.new(movie, "#{tmp_path}/timeout.mp4") - expect { transcoder.run }.not_to raise_error(FFMPEG::Error, /Process hung/) + expect { transcoder.run }.to raise_error(FFMPEG::Error, /Errors: no output file created/) end after do @@ -208,22 +252,134 @@ module FFMPEG end end - context "with :validate => false set as transcoding_options" do - let(:transcoder) { Transcoder.new(movie, "tmp.mp4", {},{:validate => false}) } + describe 'watermarking' do + context 'with default transcoder_options' do - before { transcoder.stub(:transcode_movie) } - after { FileUtils.rm_f "#{tmp_path}/tmp.mp4" } + it 'should transcode the movie with the watermark' do + options = { watermark: "#{fixture_path}/images/watermark.png", watermark_filter: { position: 'RB' } } + transcoder = Transcoder.new(movie, "#{tmp_path}/watermarked.mp4", options) + expect { transcoder.run }.not_to raise_error + end + end + end + + describe 'transcoding_options' do + let(:transcoder) { Transcoder.new(movie, "#{tmp_path}/tmp.mp4", options, transcoding_options) } + + context 'with validate: false' do + let(:options) { {} } + let(:transcoding_options) { {validate: false} } - it "should not validate the movie output" do - transcoder.should_not_receive(:validate_output_file) - transcoder.stub(:encoded) - transcoder.run + before { allow(transcoder).to receive(:transcode_movie) } + after { FileUtils.rm_f "#{tmp_path}/tmp.mp4" } + + it 'should not validate the movie output' do + expect(transcoder).to_not receive(:validate_output_file) + allow(transcoder).to receive(:encoded) + transcoder.run + end + + it 'should not return Movie object' do + allow(transcoder).to receive(:validate_output_file) + expect(transcoder).to_not receive(:encoded) + expect(transcoder.run).to eq(nil) + end end - it "should not return Movie object" do - transcoder.stub(:validate_output_file) - transcoder.should_not_receive(:encoded) - transcoder.run.should == nil + context 'with custom options' do + let(:options) { { + video_codec: 'libx264', + custom: %w(-map 0:0 -map 0:1) + } } + let(:transcoding_options) { {} } + + it 'should not raise an error' do + expect { transcoder.run }.to_not raise_error + end + + it 'should add the custom options to the command' do + expect(transcoder.command.join(' ')).to include('-map 0:0 -map 0:1') + end + end + + context 'with input' do + let(:input_path) { 'path/img_%03d.gif' } + + context 'is a movie' do + context 'and no input is specified' do + let(:transcoder) { Transcoder.new(movie, 'tmp.mp4') } + + it "should use the movie's path" do + expect(transcoder.input).to eq(movie.path) + end + + it 'should add the input to the shell command' do + expect(transcoder.command.join(' ')).to include(" -i #{transcoder.input}") + end + end + + context 'and an input is also specified' do + let(:transcoder) { Transcoder.new(movie, 'tmp.mp4', {}, input: input_path) } + + it 'should use the provided input' do + expect(transcoder.input).to eq(input_path) + end + end + end + + context 'is a path spec' do + let(:transcoder) { Transcoder.new(movie, 'tmp.mp4', {}, input: input_path) } + + it "should use the input path" do + expect(transcoder.input).to eq(input_path) + end + end + end + + context 'with input_options' do + let(:option) { {framerate: '1/5'} } + let(:transcoder) { Transcoder.new(movie, 'tmp.mp4', {}, input_options: option) } + + it 'should add the input_options before the input' do + expect(transcoder.command.join(' ')).to include("-framerate 1/5 -i #{transcoder.input}") + end + + context 'to create a slideshow' do + let(:file_spec) { "#{fixture_path}/images/img_%03d.jpeg"} + let(:output) { "#{tmp_path}/slideshow.mp4"} + let(:transcoder) { Transcoder.new(movie, output, {}, input: file_spec, input_options: option) } + + it 'should add the input_options before the input' do + expect(transcoder.command.join(' ')).to include("-framerate 1/5 -i #{file_spec}") + end + + it 'should not raise an error' do + expect { transcoder.run }.to_not raise_error + end + + it 'should produce the slideshow' do + encoded = transcoder.run + expect(encoded.duration).to eq(25) + end + + context 'with source files where file type name does not match the image type' do + let(:file_spec) { "#{fixture_path}/images/wrong_type/img_%03d.tiff"} + let(:output) { "#{tmp_path}/slideshow_fail.mp4"} + + it 'should raise an error' do + expect { transcoder.run }.to raise_error(FFMPEG::Error, /encoded file is invalid/) + end + end + + context 'with no movie defined' do + let(:movie) { nil } + + it 'should not raise an error' do + expect { transcoder.run }.to_not raise_error + end + end + + end end end end diff --git a/spec/fixtures/images/img_001.jpeg b/spec/fixtures/images/img_001.jpeg new file mode 100644 index 00000000..96175d33 Binary files /dev/null and b/spec/fixtures/images/img_001.jpeg differ diff --git a/spec/fixtures/images/img_002.jpeg b/spec/fixtures/images/img_002.jpeg new file mode 100644 index 00000000..cc029f7b Binary files /dev/null and b/spec/fixtures/images/img_002.jpeg differ diff --git a/spec/fixtures/images/img_003.jpeg b/spec/fixtures/images/img_003.jpeg new file mode 100644 index 00000000..36f2f363 Binary files /dev/null and b/spec/fixtures/images/img_003.jpeg differ diff --git a/spec/fixtures/images/img_004.jpeg b/spec/fixtures/images/img_004.jpeg new file mode 100644 index 00000000..da617b54 Binary files /dev/null and b/spec/fixtures/images/img_004.jpeg differ diff --git a/spec/fixtures/images/img_005.jpeg b/spec/fixtures/images/img_005.jpeg new file mode 100644 index 00000000..47eb2147 Binary files /dev/null and b/spec/fixtures/images/img_005.jpeg differ diff --git a/spec/fixtures/images/watermark.png b/spec/fixtures/images/watermark.png new file mode 100644 index 00000000..2b5e3843 Binary files /dev/null and b/spec/fixtures/images/watermark.png differ diff --git a/spec/fixtures/images/wrong_type/img_001.tiff b/spec/fixtures/images/wrong_type/img_001.tiff new file mode 100644 index 00000000..96175d33 Binary files /dev/null and b/spec/fixtures/images/wrong_type/img_001.tiff differ diff --git a/spec/fixtures/images/wrong_type/img_002.tiff b/spec/fixtures/images/wrong_type/img_002.tiff new file mode 100644 index 00000000..cc029f7b Binary files /dev/null and b/spec/fixtures/images/wrong_type/img_002.tiff differ diff --git a/spec/fixtures/movies/file+with+data&streams=works?.mp4 b/spec/fixtures/movies/file+with+data&streams=works?.mp4 new file mode 100644 index 00000000..abf9b97f Binary files /dev/null and b/spec/fixtures/movies/file+with+data&streams=works?.mp4 differ diff --git a/spec/fixtures/movies/file_with_data_streams.mp4 b/spec/fixtures/movies/file_with_data_streams.mp4 new file mode 100644 index 00000000..abf9b97f Binary files /dev/null and b/spec/fixtures/movies/file_with_data_streams.mp4 differ diff --git a/spec/fixtures/movies/ios9.mov b/spec/fixtures/movies/ios9.mov new file mode 100644 index 00000000..7778bb88 Binary files /dev/null and b/spec/fixtures/movies/ios9.mov differ diff --git a/spec/fixtures/movies/ios_rotate0.mov b/spec/fixtures/movies/ios_rotate0.mov new file mode 100644 index 00000000..8d5d6c13 Binary files /dev/null and b/spec/fixtures/movies/ios_rotate0.mov differ diff --git a/spec/fixtures/movies/ios_rotate180.mov b/spec/fixtures/movies/ios_rotate180.mov new file mode 100644 index 00000000..7c7a3e27 Binary files /dev/null and b/spec/fixtures/movies/ios_rotate180.mov differ diff --git a/spec/fixtures/movies/ios_rotate270.mov b/spec/fixtures/movies/ios_rotate270.mov new file mode 100644 index 00000000..00f407d1 Binary files /dev/null and b/spec/fixtures/movies/ios_rotate270.mov differ diff --git a/spec/fixtures/movies/ios_rotate90.mov b/spec/fixtures/movies/ios_rotate90.mov new file mode 100644 index 00000000..49321468 Binary files /dev/null and b/spec/fixtures/movies/ios_rotate90.mov differ diff --git a/spec/fixtures/movies/multi_audio_movie.mp4 b/spec/fixtures/movies/multi_audio_movie.mp4 new file mode 100644 index 00000000..f5c12d3e Binary files /dev/null and b/spec/fixtures/movies/multi_audio_movie.mp4 differ diff --git a/spec/fixtures/outputs/file_with_bad_json.txt b/spec/fixtures/outputs/file_with_bad_json.txt new file mode 100644 index 00000000..03d78481 --- /dev/null +++ b/spec/fixtures/outputs/file_with_bad_json.txt @@ -0,0 +1,4 @@ +dyld: Library not loaded: /usr/local/opt/openh264/lib/libopenh264.1.dylib + Referenced from: /usr/local/bin/ffprobe + Reason: image not found +[1] 46696 abort /usr/local/bin/ffprobe -i spec/fixtures/village.mov -print_format json diff --git a/spec/fixtures/outputs/file_with_colorspace_with_parenthesis_but_no_comma.txt b/spec/fixtures/outputs/file_with_colorspace_with_parenthesis_but_no_comma.txt deleted file mode 100644 index 3d1b27c2..00000000 --- a/spec/fixtures/outputs/file_with_colorspace_with_parenthesis_but_no_comma.txt +++ /dev/null @@ -1,28 +0,0 @@ -ffmpeg version 1.2.1 Copyright (c) 2000-2013 the FFmpeg developers - built on Sep 15 2013 00:26:59 with Apple LLVM version 4.2 (clang-425.0.28) (based on LLVM 3.2svn) - configuration: --prefix=/usr/local/Cellar/ffmpeg/1.2.1 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-nonfree --enable-hardcoded-tables --enable-avresample --enable-vda --cc=cc --host-cflags= --host-ldflags= --enable-libx264 --enable-libfaac --enable-libmp3lame --enable-libxvid - libavutil 52. 18.100 / 52. 18.100 - libavcodec 54. 92.100 / 54. 92.100 - libavformat 54. 63.104 / 54. 63.104 - libavdevice 54. 3.103 / 54. 3.103 - libavfilter 3. 42.103 / 3. 42.103 - libswscale 2. 2.100 / 2. 2.100 - libswresample 0. 17.102 / 0. 17.102 - libpostproc 52. 2.100 / 52. 2.100 -Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '../movies/awesome_widescreen.mov': - Metadata: - major_brand : isom - minor_version : 512 - compatible_brands: isomiso2avc1mp41 - creation_time : 1970-01-01 00:00:00 - encoder : Lavf52.61.0 - Duration: 00:00:07.34, start: 0.000000, bitrate: 321 kb/s - Stream #0:0(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(tv), 320x180 [SAR 1:1 DAR 16:9], 291 kb/s, 10 fps, 10 tbr, 10 tbn, 20 tbc - Metadata: - creation_time : 1970-01-01 00:00:00 - handler_name : VideoHandler - Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 22050 Hz, mono, fltp, 31 kb/s - Metadata: - creation_time : 1970-01-01 00:00:00 - handler_name : SoundHandler -At least one output file must be specified diff --git a/spec/fixtures/outputs/file_with_complex_colorspace_and_decimal_fps.txt b/spec/fixtures/outputs/file_with_complex_colorspace_and_decimal_fps.txt deleted file mode 100644 index c829bcb3..00000000 --- a/spec/fixtures/outputs/file_with_complex_colorspace_and_decimal_fps.txt +++ /dev/null @@ -1,27 +0,0 @@ -ffmpeg version git-2013-09-06-bcd1c20 Copyright (c) 2000-2013 the FFmpeg developers - built on Sep 6 2013 01:14:28 with gcc 4.6 (Ubuntu/Linaro 4.6.3-1ubuntu5) - configuration: --enable-gpl --enable-version3 --enable-nonfree --enable-x11grab --enable-vdpau --enable-runtime-cpudetect --enable-vaapi --enable-vda --enable-gnutls --enable-libass --enable-libmodplug --enable-libpulse --enable-librtmp --enable-libvo-aacenc --enable-libvo-amrwbenc --enable-openal - libavutil 52. 43.100 / 52. 43.100 - libavcodec 55. 31.101 / 55. 31.101 - libavformat 55. 16.101 / 55. 16.101 - libavdevice 55. 3.100 / 55. 3.100 - libavfilter 3. 83.104 / 3. 83.104 - libswscale 2. 5.100 / 2. 5.100 - libswresample 0. 17.103 / 0. 17.103 - libpostproc 52. 3.100 / 52. 3.100 -Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'ADHP_E06.mov': - Metadata: - major_brand : qt - minor_version : 537199360 - compatible_brands: qt - creation_time : 2013-06-11 00:23:06 - Duration: 00:00:12.05, start: 0.000000, bitrate: 6660 kb/s - Stream #0:0(eng): Audio: aac (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 124 kb/s (default) - Metadata: - creation_time : 2013-06-11 00:23:06 - handler_name : Apple Alias Data Handler - Stream #0:1(eng): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709), 960x540, 6520 kb/s, 23.98 fps, 23.98 tbr, 24k tbn, 48k tbc (default) - Metadata: - creation_time : 2013-06-11 00:23:07 - handler_name : Apple Alias Data Handler -At least one output file must be specified \ No newline at end of file diff --git a/spec/fixtures/outputs/file_with_iso-8859-1.txt b/spec/fixtures/outputs/file_with_iso-8859-1.txt index fcd6f027..e0d06ca7 100644 --- a/spec/fixtures/outputs/file_with_iso-8859-1.txt +++ b/spec/fixtures/outputs/file_with_iso-8859-1.txt @@ -1,35 +1,106 @@ -FFmpeg version 0.6.1, Copyright (c) 2000-2010 the FFmpeg developers - built on Nov 18 2010 13:51:50 with gcc 4.2.1 (Apple Inc. build 5664) - configuration: --disable-debug --prefix=/usr/local/Cellar/ffmpeg/0.6.1 --enable-shared --enable-pthreads --enable-nonfree --enable-gpl --disable-indev=jack --enable-libx264 --enable-libfaac --enable-libfaad --enable-libmp3lame --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libxvid - libavutil 50.15. 1 / 50.15. 1 - libavcodec 52.72. 2 / 52.72. 2 - libavformat 52.64. 2 / 52.64. 2 - libavdevice 52. 2. 0 / 52. 2. 0 - libswscale 0.11. 0 / 0.11. 0 -[mov,mp4,m4a,3gp,3g2,mj2 @ 0x10180b000]multiple edit list entries, a/v desync might occur, patch welcome -[mov,mp4,m4a,3gp,3g2,mj2 @ 0x10180b000]max_analyze_duration reached - -Seems stream 0 codec frame rate differs from container frame rate: 5000.00 (5000/1) -> 25.00 (25/1) -Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'spec/fixtures/movies/rails3upgrade.mov': - Metadata: - major_brand : qt - minor_version : 537199360 - compatible_brands: qt - Duration: 00:24:42.61, start: 0.000000, bitrate: 546 kb/s - Chapter #0.0: start 142.240000, end 532.679600 - Metadata: - title : R - Chapter #0.1: start 532.679600, end 788.119600 - Metadata: - title : r - Chapter #0.2: start 788.119600, end 1160.159600 - Metadata: - title : b - Chapter #0.3: start 1160.159600, end 1340.359200 - Metadata: - title : A - Stream #0.0(eng): Video: h264, yuv420p, 1000x600, 480 kb/s, 25 fps, 25 tbr, 2500 tbn, 5k tbc - Stream #0.1(eng): Data: tmcd / 0x64636D74 - Stream #0.2(eng): Audio: aac, 44100 Hz, mono, s16, 64 kb/s - Stream #0.3(eng): Subtitle: text / 0x74786574, 0 kb/s -At least one output file must be specified +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "Main", + "codec_type": "video", + "codec_time_base": "1/38400", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 640, + "height": 480, + "has_b_frames": 1, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "4:3", + "pix_fmt": "yuv420p", + "level": 30, + "r_frame_rate": "15/1", + "avg_frame_rate": "3075/182", + "time_base": "1/19200", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 139776, + "duration": "7.280000", + "bit_rate": "374500", + "nb_frames": "123", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "language": "eng", + "handler_name": "DataHandler" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "codec_type": "audio", + "codec_time_base": "1/44100", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_fmt": "fltp", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/44100", + "start_pts": 3793, + "start_time": "0.086009", + "duration_ts": 328705, + "duration": "7.453628", + "bit_rate": "76068", + "nb_frames": "322", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "language": "eng", + "handler_name": "DataHandler" + } + } + ], + "format": { + "filename": "spec/fixtures/movies/iso-8859-1.mov", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "7.391000", + "size": "417938", + "bit_rate": "452375", + "probe_score": 100, + "tags": { + "major_brand": "qt ", + "minor_version": "512", + "compatible_brands": "qt ", + "title": "��R��r��b��A 2011年秋季,Peter Norvig和Sebastian Thrun在斯坦福大学开办了一个由175在校学生出席的的“人工智能”课程——这同时还有超过十万的学生通过互动直", + "encoder": "Lavf55.19.104" + } + } +} diff --git a/spec/fixtures/outputs/file_with_no_audio.txt b/spec/fixtures/outputs/file_with_no_audio.txt index 12faa232..9571120a 100644 --- a/spec/fixtures/outputs/file_with_no_audio.txt +++ b/spec/fixtures/outputs/file_with_no_audio.txt @@ -1,18 +1,65 @@ -FFmpeg version 0.6.1, Copyright (c) 2000-2010 the FFmpeg developers - built on Nov 18 2010 13:51:50 with gcc 4.2.1 (Apple Inc. build 5664) - configuration: --disable-debug --prefix=/usr/local/Cellar/ffmpeg/0.6.1 --enable-shared --enable-pthreads --enable-nonfree --enable-gpl --disable-indev=jack --enable-libx264 --enable-libfaac --enable-libfaad --enable-libmp3lame --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libxvid - libavutil 50.15. 1 / 50.15. 1 - libavcodec 52.72. 2 / 52.72. 2 - libavformat 52.64. 2 / 52.64. 2 - libavdevice 52. 2. 0 / 52. 2. 0 - libswscale 0.11. 0 / 0.11. 0 - -Seems stream 0 codec frame rate differs from container frame rate: 2500.00 (2500/1) -> 25.00 (25/1) -Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'broken.mov': - Metadata: - major_brand : qt - minor_version : 537199360 - compatible_brands: qt - Duration: 00:00:23.12, start: 0.000000, bitrate: 28033 kb/s - Stream #0.0(eng): Video: Apple ProRes 422, 410x620, 28029 kb/s, 25 fps, 25 tbr, 2500 tbn, 2500 tbc -At least one output file must be specified +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "Main", + "codec_type": "video", + "codec_time_base": "1/38400", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 640, + "height": 480, + "has_b_frames": 1, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "4:3", + "pix_fmt": "yuv420p", + "level": 30, + "r_frame_rate": "15/1", + "avg_frame_rate": "3075/182", + "time_base": "1/19200", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 139776, + "duration": "7.280000", + "bit_rate": "374500", + "nb_frames": "123", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "language": "eng", + "handler_name": "DataHandler" + } + } + ], + "format": { + "filename": "spec/fixtures/movies/no_audio.mov", + "nb_streams": 1, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "7.280000", + "size": "343446", + "bit_rate": "377413", + "probe_score": 100, + "tags": { + "major_brand": "qt ", + "minor_version": "512", + "compatible_brands": "qt ", + "encoder": "Lavf55.19.104" + } + } +} diff --git a/spec/fixtures/outputs/file_with_non_supported_audio.txt b/spec/fixtures/outputs/file_with_non_supported_audio.txt deleted file mode 100644 index 140bdf4c..00000000 --- a/spec/fixtures/outputs/file_with_non_supported_audio.txt +++ /dev/null @@ -1,29 +0,0 @@ -ffmpeg version 0.8, Copyright (c) 2000-2011 the FFmpeg developers - built on Jul 20 2011 10:41:26 with gcc 4.2.1 (Apple Inc. build 5664) - configuration: --prefix=/usr/local/Cellar/ffmpeg/0.8 --enable-gpl --enable-version3 --enable-nonfree --enable-hardcoded-tables --enable-libx264 --enable-libfaac --enable-libmp3lame --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libxvid - libavutil 51. 9. 1 / 51. 9. 1 - libavcodec 53. 7. 0 / 53. 7. 0 - libavformat 53. 4. 0 / 53. 4. 0 - libavdevice 53. 1. 1 / 53. 1. 1 - libavfilter 2. 23. 0 / 2. 23. 0 - libswscale 2. 0. 0 / 2. 0. 0 - libpostproc 51. 2. 0 / 51. 2. 0 -[aac @ 0x101820400] Audio object type 23 is not supported. - Last message repeated 471 times -[mov,mp4,m4a,3gp,3g2,mj2 @ 0x101807c00] max_analyze_duration 5000000 reached at 5002667 - -Seems stream 1 codec frame rate differs from container frame rate: 96000.00 (96000/1) -> 48000.00 (96000/2) -Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'no_audio_channel_info.mov': - Metadata: - major_brand : qt - minor_version : 537199360 - compatible_brands: qt - creation_time : 2009-05-26 14:36:22 - Duration: 00:13:15.81, start: 0.000000, bitrate: 4825 kb/s - Stream #0.0(eng): Audio: aac, stereo, 182 kb/s - Metadata: - creation_time : 2009-05-26 14:36:22 - Stream #0.1(eng): Video: h264 (Main), yuv420p, 768x434, 4636 kb/s, 25 fps, 48k tbr, 48k tbn, 96k tbc - Metadata: - creation_time : 2009-05-26 14:36:22 -At least one output file must be specified diff --git a/spec/fixtures/outputs/file_with_non_supported_audio_and_video_stderr.txt b/spec/fixtures/outputs/file_with_non_supported_audio_and_video_stderr.txt new file mode 100644 index 00000000..451de92a --- /dev/null +++ b/spec/fixtures/outputs/file_with_non_supported_audio_and_video_stderr.txt @@ -0,0 +1,28 @@ +ffprobe version 2.1.1-static Copyright (c) 2007-2013 the FFmpeg developers + built on Feb 4 2014 13:01:52 with gcc 4.6 (Ubuntu/Linaro 4.6.3-1ubuntu5) + configuration: --prefix=/home/ryan/Source/ffmpeg-static/target --extra-cflags='-I/home/ryan/Source/ffmpeg-static/target/include -static' --extra-ldflags='-L/home/ryan/Source/ffmpeg-static/target/lib -lm -static' --extra-version=static --disable-debug --disable-shared --enable-static --extra-cflags=--static --disable-ffplay --disable-ffserver --disable-doc --enable-gpl --enable-pthreads --enable-postproc --enable-gray --enable-runtime-cpudetect --disable-libfaac --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libx264 --enable-libxvid --enable-bzlib --enable-zlib --enable-nonfree --enable-version3 --enable-libvpx --disable-devices --disable-decoder=aac + libavutil 52. 48.101 / 52. 48.101 + libavcodec 55. 39.101 / 55. 39.101 + libavformat 55. 19.104 / 55. 19.104 + libavdevice 55. 5.100 / 55. 5.100 + libavfilter 3. 90.100 / 3. 90.100 + libswscale 2. 5.101 / 2. 5.101 + libswresample 0. 17.104 / 0. 17.104 + libpostproc 52. 3.100 / 52. 3.100 +Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'bigbucksbunny_trailer_720p.mov': + Metadata: + major_brand : qt + minor_version : 537199360 + compatible_brands: qt + creation_time : 2008-03-18 12:45:48 + Duration: 00:00:33.00, start: 0.000000, bitrate: 4318 kb/s + Stream #0:0(eng): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709), 1280x720, 3945 kb/s, 25 fps, 25 tbr, 600 tbn, 1200 tbc (default) + Metadata: + creation_time : 2008-03-18 12:45:48 + handler_name : Apple Alias Data Handler + Stream #0:1(eng): Audio: aac (mp4a / 0x6134706D), 48000 Hz, 5.1(side), 428 kb/s (default) + Metadata: + creation_time : 2008-03-18 12:45:48 + handler_name : Apple Alias Data Handler +Unsupported codec with id 37923 for input stream 0 +Unsupported codec with id 86018 for input stream 1 diff --git a/spec/fixtures/outputs/file_with_non_supported_audio_and_video_stdout.txt b/spec/fixtures/outputs/file_with_non_supported_audio_and_video_stdout.txt new file mode 100644 index 00000000..e5f95ab5 --- /dev/null +++ b/spec/fixtures/outputs/file_with_non_supported_audio_and_video_stdout.txt @@ -0,0 +1,104 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "Main", + "codec_type": "video", + "codec_time_base": "1/1200", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 1280, + "height": 720, + "has_b_frames": 0, + "sample_aspect_ratio": "0:1", + "display_aspect_ratio": "0:1", + "pix_fmt": "yuv420p", + "level": 31, + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/600", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 19488, + "duration": "32.480000", + "bit_rate": "3945184", + "nb_frames": "812", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "creation_time": "2008-03-18 12:45:48", + "language": "eng", + "handler_name": "Apple Alias Data Handler" + } + }, + { + "index": 1, + "codec_type": "audio", + "codec_time_base": "0/1", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1(side)", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/48000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1584128, + "duration": "33.002667", + "bit_rate": "428605", + "nb_frames": "1547", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "creation_time": "2008-03-18 12:45:48", + "language": "eng", + "handler_name": "Apple Alias Data Handler" + } + } + ], + "format": { + "filename": "bigbucksbunny_trailer_720p.mov", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "32.995000", + "size": "17810888", + "bit_rate": "4318445", + "probe_score": 100, + "tags": { + "major_brand": "qt ", + "minor_version": "537199360", + "compatible_brands": "qt ", + "creation_time": "2008-03-18 12:45:48" + } + } +} diff --git a/spec/fixtures/outputs/file_with_non_supported_audio_stderr.txt b/spec/fixtures/outputs/file_with_non_supported_audio_stderr.txt new file mode 100644 index 00000000..a00e514d --- /dev/null +++ b/spec/fixtures/outputs/file_with_non_supported_audio_stderr.txt @@ -0,0 +1,27 @@ +ffprobe version 2.1.1-static Copyright (c) 2007-2013 the FFmpeg developers + built on Feb 4 2014 13:01:52 with gcc 4.6 (Ubuntu/Linaro 4.6.3-1ubuntu5) + configuration: --prefix=/home/ryan/Source/ffmpeg-static/target --extra-cflags='-I/home/ryan/Source/ffmpeg-static/target/include -static' --extra-ldflags='-L/home/ryan/Source/ffmpeg-static/target/lib -lm -static' --extra-version=static --disable-debug --disable-shared --enable-static --extra-cflags=--static --disable-ffplay --disable-ffserver --disable-doc --enable-gpl --enable-pthreads --enable-postproc --enable-gray --enable-runtime-cpudetect --disable-libfaac --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libx264 --enable-libxvid --enable-bzlib --enable-zlib --enable-nonfree --enable-version3 --enable-libvpx --disable-devices --disable-decoder=aac + libavutil 52. 48.101 / 52. 48.101 + libavcodec 55. 39.101 / 55. 39.101 + libavformat 55. 19.104 / 55. 19.104 + libavdevice 55. 5.100 / 55. 5.100 + libavfilter 3. 90.100 / 3. 90.100 + libswscale 2. 5.101 / 2. 5.101 + libswresample 0. 17.104 / 0. 17.104 + libpostproc 52. 3.100 / 52. 3.100 +Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'bigbucksbunny_trailer_720p.mov': + Metadata: + major_brand : qt + minor_version : 537199360 + compatible_brands: qt + creation_time : 2008-03-18 12:45:48 + Duration: 00:00:33.00, start: 0.000000, bitrate: 4318 kb/s + Stream #0:0(eng): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709), 1280x720, 3945 kb/s, 25 fps, 25 tbr, 600 tbn, 1200 tbc (default) + Metadata: + creation_time : 2008-03-18 12:45:48 + handler_name : Apple Alias Data Handler + Stream #0:1(eng): Audio: aac (mp4a / 0x6134706D), 48000 Hz, 5.1(side), 428 kb/s (default) + Metadata: + creation_time : 2008-03-18 12:45:48 + handler_name : Apple Alias Data Handler +Unsupported codec with id 86018 for input stream 1 diff --git a/spec/fixtures/outputs/file_with_non_supported_audio_stdout.txt b/spec/fixtures/outputs/file_with_non_supported_audio_stdout.txt new file mode 100644 index 00000000..e5f95ab5 --- /dev/null +++ b/spec/fixtures/outputs/file_with_non_supported_audio_stdout.txt @@ -0,0 +1,104 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "Main", + "codec_type": "video", + "codec_time_base": "1/1200", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 1280, + "height": 720, + "has_b_frames": 0, + "sample_aspect_ratio": "0:1", + "display_aspect_ratio": "0:1", + "pix_fmt": "yuv420p", + "level": 31, + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/600", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 19488, + "duration": "32.480000", + "bit_rate": "3945184", + "nb_frames": "812", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "creation_time": "2008-03-18 12:45:48", + "language": "eng", + "handler_name": "Apple Alias Data Handler" + } + }, + { + "index": 1, + "codec_type": "audio", + "codec_time_base": "0/1", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1(side)", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/48000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1584128, + "duration": "33.002667", + "bit_rate": "428605", + "nb_frames": "1547", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "creation_time": "2008-03-18 12:45:48", + "language": "eng", + "handler_name": "Apple Alias Data Handler" + } + } + ], + "format": { + "filename": "bigbucksbunny_trailer_720p.mov", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "32.995000", + "size": "17810888", + "bit_rate": "4318445", + "probe_score": 100, + "tags": { + "major_brand": "qt ", + "minor_version": "537199360", + "compatible_brands": "qt ", + "creation_time": "2008-03-18 12:45:48" + } + } +} diff --git a/spec/fixtures/outputs/file_with_surround_sound.txt b/spec/fixtures/outputs/file_with_surround_sound.txt index 3dd412cb..a9ae2d0e 100644 --- a/spec/fixtures/outputs/file_with_surround_sound.txt +++ b/spec/fixtures/outputs/file_with_surround_sound.txt @@ -1,19 +1,107 @@ -FFmpeg version 0.6.1, Copyright (c) 2000-2010 the FFmpeg developers - built on Nov 18 2010 13:51:50 with gcc 4.2.1 (Apple Inc. build 5664) - configuration: --disable-debug --prefix=/usr/local/Cellar/ffmpeg/0.6.1 --enable-shared --enable-pthreads --enable-nonfree --enable-gpl --disable-indev=jack --enable-libx264 --enable-libfaac --enable-libfaad --enable-libmp3lame --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libxvid - libavutil 50.15. 1 / 50.15. 1 - libavcodec 52.72. 2 / 52.72. 2 - libavformat 52.64. 2 / 52.64. 2 - libavdevice 52. 2. 0 / 52. 2. 0 - libswscale 0.11. 0 / 0.11. 0 - -Seems stream 0 codec frame rate differs from container frame rate: 1200.00 (1200/1) -> 25.00 (25/1) -Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'bigbucksbunny_trailer_720p.mov': - Metadata: - major_brand : qt - minor_version : 537199360 - compatible_brands: qt - Duration: 00:00:33.00, start: 0.000000, bitrate: 4317 kb/s - Stream #0.0(eng): Video: h264, yuv420p, 1280x720, 3945 kb/s, 25 fps, 25 tbr, 600 tbn, 1200 tbc - Stream #0.1(eng): Audio: aac, 48000 Hz, 5.1, s16, 428 kb/s -At least one output file must be specified +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "Main", + "codec_type": "video", + "codec_time_base": "1/1200", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 1280, + "height": 720, + "has_b_frames": 0, + "sample_aspect_ratio": "0:1", + "display_aspect_ratio": "0:1", + "pix_fmt": "yuv420p", + "level": 31, + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/600", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 19488, + "duration": "32.480000", + "bit_rate": "3945184", + "nb_frames": "812", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "creation_time": "2008-03-18 12:45:48", + "language": "eng", + "handler_name": "Apple Alias Data Handler" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "codec_type": "audio", + "codec_time_base": "1/48000", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/48000", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 1584128, + "duration": "33.002667", + "bit_rate": "428605", + "nb_frames": "1547", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "creation_time": "2008-03-18 12:45:48", + "language": "eng", + "handler_name": "Apple Alias Data Handler" + } + } + ], + "format": { + "filename": "spec/fixtures/movies/bigbucksbunny_trailer_720p.mov", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "32.995000", + "size": "17810888", + "bit_rate": "4318445", + "probe_score": 100, + "tags": { + "major_brand": "qt ", + "minor_version": "537199360", + "compatible_brands": "qt ", + "creation_time": "2008-03-18 12:45:48" + } + } +} diff --git a/spec/fixtures/outputs/file_with_weird_dar.txt b/spec/fixtures/outputs/file_with_weird_dar.txt index 05425ea8..0a0dbb61 100644 --- a/spec/fixtures/outputs/file_with_weird_dar.txt +++ b/spec/fixtures/outputs/file_with_weird_dar.txt @@ -1,24 +1,106 @@ -ffmpeg version 0.8.5, Copyright (c) 2000-2011 the FFmpeg developers - built on Oct 17 2011 12:25:58 with gcc 4.2.1 (Apple Inc. build 5666) (dot 3) - configuration: --prefix=/usr/local/Cellar/ffmpeg/0.8.5 --enable-shared --enable-gpl --enable-version3 --enable-nonfree --enable-hardcoded-tables --cc=/usr/bin/gcc-4.2 --enable-libx264 --enable-libfaac --enable-libmp3lame --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libxvid - libavutil 51. 9. 1 / 51. 9. 1 - libavcodec 53. 7. 0 / 53. 7. 0 - libavformat 53. 4. 0 / 53. 4. 0 - libavdevice 53. 1. 1 / 53. 1. 1 - libavfilter 2. 23. 0 / 2. 23. 0 - libswscale 2. 0. 0 / 2. 0. 0 - libpostproc 51. 2. 0 / 51. 2. 0 -Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/druiden/Movies/clips/metal_gear-aspect-ratio-fail.mov': - Metadata: - major_brand : qt - minor_version : 537199360 - compatible_brands: qt - creation_time : 2011-05-31 01:47:42 - Duration: 00:02:40.63, start: 0.000000, bitrate: 17495 kb/s - Stream #0.0(eng): Video: h264 (Main), yuv420p, 1280x720, 15955 kb/s, PAR 16777216:16777216 DAR 0:1, 30 fps, 30 tbr, 30 tbn, 60 tbc - Metadata: - creation_time : 2011-05-31 01:47:42 - Stream #0.1(eng): Audio: pcm_s16le, 48000 Hz, 2 channels, s16, 1536 kb/s - Metadata: - creation_time : 2011-05-31 01:47:42 -At least one output file must be specified \ No newline at end of file +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "High", + "codec_type": "video", + "codec_time_base": "1/60", + "codec_tag_string": "avc1", + "codec_tag": "0x31637661", + "width": 1280, + "height": 720, + "has_b_frames": 0, + "sample_aspect_ratio": "0:1", + "display_aspect_ratio": "0:1", + "pix_fmt": "yuv420p", + "level": 31, + "r_frame_rate": "30/1", + "avg_frame_rate": "30/1", + "time_base": "1/60", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 9638, + "duration": "160.633333", + "bit_rate": "1272214", + "nb_frames": "4819", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "language": "und", + "handler_name": "VideoHandler" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "codec_type": "audio", + "codec_time_base": "1/44100", + "codec_tag_string": "mp4a", + "codec_tag": "0x6134706d", + "sample_fmt": "fltp", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/44100", + "start_pts": 0, + "start_time": "0.000000", + "duration_ts": 7084032, + "duration": "160.635646", + "bit_rate": "192007", + "nb_frames": "6918", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + }, + "tags": { + "creation_time": "2014-02-02 19:30:25", + "language": "und", + "handler_name": "IsoMedia File Produced by Google, 5-11-2011" + } + } + ], + "format": { + "filename": "Metal_gear-aspect-ratio-fail.mov-BeAQfhzGg9c.mp4", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "mov,mp4,m4a,3gp,3g2,mj2", + "format_long_name": "QuickTime / MOV", + "start_time": "0.000000", + "duration": "160.635000", + "size": "29455290", + "bit_rate": "1466942", + "probe_score": 100, + "tags": { + "major_brand": "mp42", + "minor_version": "0", + "compatible_brands": "isommp42", + "creation_time": "2014-02-02 19:30:24" + } + } +} diff --git a/spec/fixtures/outputs/file_with_weird_sar.txt b/spec/fixtures/outputs/file_with_weird_sar.txt index 037553fc..26f08c4d 100644 --- a/spec/fixtures/outputs/file_with_weird_sar.txt +++ b/spec/fixtures/outputs/file_with_weird_sar.txt @@ -1,16 +1,88 @@ -ffmpeg version N-57291-g80b8f78 Copyright (c) 2000-2013 the FFmpeg developers - built on Oct 21 2013 23:07:04 with Apple LLVM version 5.0 (clang-500.2.75) (based on LLVM 3.3svn) - configuration: --cc=clang --enable-gpl --enable-shared --enable-libmp3lame --enable-libxvid --enable-libx264 --enable-pthreads --enable-libfaac --enable-postproc --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libvorbis --enable-libgsm --enable-nonfree --enable-avfilter --enable-libtheora --enable-version3 --enable-librtmp --enable-openssl --enable-libvpx - libavutil 52. 47.101 / 52. 47.101 - libavcodec 55. 37.102 / 55. 37.102 - libavformat 55. 19.103 / 55. 19.103 - libavdevice 55. 4.100 / 55. 4.100 - libavfilter 3. 88.102 / 3. 88.102 - libswscale 2. 5.101 / 2. 5.101 - libswresample 0. 17.104 / 0. 17.104 - libpostproc 52. 3.100 / 52. 3.100 -Input #0, mpeg, from 'spec/fixtures/movies/weird_aspect.small.mpg': - Duration: 00:00:01.44, start: 0.220000, bitrate: 1706 kb/s - Stream #0:0[0x1e0]: Video: mpeg1video, yuv420p(tv), 352x288 [SAR 0:1 DAR 704:405], 1500 kb/s, 25 fps, 25 tbr, 90k tbn, 25 tbc - Stream #0:1[0x1c0]: Audio: mp2, 44100 Hz, stereo, s16p, 224 kb/s -At least one output file must be specified \ No newline at end of file +{ + "streams": [ + { + "index": 0, + "codec_name": "mpeg1video", + "codec_type": "video", + "codec_time_base": "1/25", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 352, + "height": 288, + "has_b_frames": 1, + "sample_aspect_ratio": "0:1", + "display_aspect_ratio": "704:405", + "pix_fmt": "yuv420p", + "level": -99, + "timecode": "00:00:00:00", + "id": "0x1e0", + "r_frame_rate": "25/1", + "avg_frame_rate": "25/1", + "time_base": "1/90000", + "start_pts": 19800, + "start_time": "0.220000", + "duration_ts": 129600, + "duration": "1.440000", + "bit_rate": "1500000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + } + }, + { + "index": 1, + "codec_name": "mp2", + "codec_type": "audio", + "codec_time_base": "1/44100", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "s16p", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "id": "0x1c0", + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/90000", + "start_pts": 19800, + "start_time": "0.220000", + "duration_ts": 105795, + "duration": "1.175500", + "bit_rate": "224000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0 + } + } + ], + "format": { + "filename": "spec/fixtures/movies/weird_aspect.small.mpg", + "nb_streams": 2, + "nb_programs": 0, + "format_name": "mpeg", + "start_time": "0.220000", + "duration": "1.440000", + "size": "307200", + "bit_rate": "1706666", + "probe_score": 52 + } +} diff --git a/spec/fixtures/sounds/bad_metadata_creation_time.wav b/spec/fixtures/sounds/bad_metadata_creation_time.wav new file mode 100644 index 00000000..b31fcf71 Binary files /dev/null and b/spec/fixtures/sounds/bad_metadata_creation_time.wav differ diff --git a/spec/fixtures/sounds/hello.wav b/spec/fixtures/sounds/hello.wav new file mode 100644 index 00000000..cca9f4c7 Binary files /dev/null and b/spec/fixtures/sounds/hello.wav differ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 960ec40b..72ddc653 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,13 +1,40 @@ +require 'simplecov' +SimpleCov.start + require 'bundler' Bundler.require require 'fileutils' +require 'webmock/rspec' +WebMock.allow_net_connect! FFMPEG.logger = Logger.new(nil) RSpec.configure do |config| config.filter_run focus: true config.run_all_when_everything_filtered = true + + config.before(:each) do + stub_request(:head, /redirect-example.com/). + 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'}). + 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'}). + to_return(status: 404, headers: { }) + stub_request(:head, /toomany-redirects-example/). + with(:headers => {'Accept'=>'*/*', 'User-Agent' => 'Ruby'}). + to_return(status: 302, headers: { + location: '/awesome%20movie.mov' + }) + + end end def fixture_path @@ -18,4 +45,19 @@ def tmp_path @tmp_path ||= File.join(File.dirname(__FILE__), "..", "tmp") end +def start_web_server + @server = WEBrick::HTTPServer.new( + Port: 8000, + DocumentRoot: "#{fixture_path}/movies", + Logger: WEBrick::Log.new(File.open(File::NULL, 'w')), + AccessLog: [] + ) + Thread.new { @server.start } +end + +def stop_web_server + @server.shutdown +end + +FileUtils.rm_rf(tmp_path) FileUtils.mkdir_p tmp_path diff --git a/spec/streamio-ffmpeg_spec.rb b/spec/streamio-ffmpeg_spec.rb index 86b65a6e..289c396e 100644 --- a/spec/streamio-ffmpeg_spec.rb +++ b/spec/streamio-ffmpeg_spec.rb @@ -7,34 +7,97 @@ end it "should be a Logger" do - FFMPEG.logger.should be_instance_of(Logger) + 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 - FFMPEG.logger.level.should == Logger::INFO + expect(FFMPEG.logger.level).to eq(Logger::INFO) end it "should be assignable" do new_logger = Logger.new(STDOUT) FFMPEG.logger = new_logger - FFMPEG.logger.should == new_logger + expect(FFMPEG.logger).to eq(new_logger) end end - describe "ffmpeg_binary" do + describe '.ffmpeg_binary' do + after(:each) do FFMPEG.ffmpeg_binary = nil end - it "should default to 'ffmpeg'" do - FFMPEG.ffmpeg_binary.should == 'ffmpeg' + 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') end - it "should be assignable" do - new_binary = '/usr/local/bin/ffmpeg' - FFMPEG.ffmpeg_binary = new_binary - FFMPEG.ffmpeg_binary.should == new_binary + it 'should be assignable' do + allow(File).to receive(:executable?).with('/new/path/to/ffmpeg') { true } + FFMPEG.ffmpeg_binary = '/new/path/to/ffmpeg' + expect(FFMPEG.ffmpeg_binary).to eq '/new/path/to/ffmpeg' + end + + it 'should raise exception if it cannot find assigned executable' do + expect { FFMPEG.ffmpeg_binary = '/new/path/to/ffmpeg' }.to raise_error(Errno::ENOENT) + end + + it 'should raise exception if it cannot find executable on path' do + 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 + + it 'should be assignable' do + allow(File).to receive(:executable?).with('/new/path/to/ffprobe') { true } + FFMPEG.ffprobe_binary = '/new/path/to/ffprobe' + expect(FFMPEG.ffprobe_binary).to eq '/new/path/to/ffprobe' + end + + it 'should raise exception if it cannot find assigned executable' do + expect { FFMPEG.ffprobe_binary = '/new/path/to/ffprobe' }.to raise_error(Errno::ENOENT) + end + + it 'should raise exception if it cannot find executable on path' do + allow(File).to receive(:executable?) { false } + expect { FFMPEG.ffprobe_binary }.to raise_error(Errno::ENOENT) + end + + end + + describe '.max_http_redirect_attempts' do + after(:each) do + FFMPEG.max_http_redirect_attempts = nil + end + + it 'should default to 10' do + expect(FFMPEG.max_http_redirect_attempts).to eq 10 + end + + it 'should be an Integer' do + expect { FFMPEG.max_http_redirect_attempts = 1.23 }.to raise_error(Errno::ENOENT) + end + + it 'should not be negative' do + expect { FFMPEG.max_http_redirect_attempts = -1 }.to raise_error(Errno::ENOENT) + end + + it 'should be assignable' do + FFMPEG.max_http_redirect_attempts = 5 + expect(FFMPEG.max_http_redirect_attempts).to eq 5 end end end \ No newline at end of file diff --git a/streamio-ffmpeg.gemspec b/streamio-ffmpeg.gemspec index 8f145bce..b0fbbd8b 100644 --- a/streamio-ffmpeg.gemspec +++ b/streamio-ffmpeg.gemspec @@ -7,12 +7,14 @@ require "ffmpeg/version" Gem::Specification.new do |s| s.name = "streamio-ffmpeg" s.version = FFMPEG::VERSION - s.authors = ["David Backeus"] - s.email = ["david@streamio.com"] + s.authors = ["Rackfish AB"] + s.email = ["support@rackfish.com", "bikeath1337.com"] s.homepage = "http://github.com/streamio/streamio-ffmpeg" s.summary = "Wraps ffmpeg to read metadata and transcodes videos." - s.add_development_dependency("rspec", "~> 2.14") + s.add_dependency('multi_json', '~> 1.8') + + s.add_development_dependency("rspec", "~> 3") s.add_development_dependency("rake", "~> 10.1") s.files = Dir.glob("lib/**/*") + %w(README.md LICENSE CHANGELOG)