Skip to content

Instantly share code, notes, and snippets.

@julik
Created February 9, 2010 14:10
Show Gist options
  • Save julik/299222 to your computer and use it in GitHub Desktop.
Save julik/299222 to your computer and use it in GitHub Desktop.
require 'open3'
require 'timecode'
# Acts as a thick layer of duct tape that helps us to operate on Quicktimes using ffmpeg.
class MovieJuggler
attr_accessor :logger
class Error < RuntimeError; end
class MovieIsCorrupted < Error; end
class ParseError < Error; end
def initialize
@logger = RAILS_DEFAULT_LOGGER
end
def self.method_missing(*msg)
new.send(*msg)
end
# Extract a frame of a movie file into a JPG image. The timecode passed are seconds in float
# - the value reported by GetTime() in the QuickTime API
def extract_frame(fractional_timecode, path_to_mov)
raise_on_nonexistent_file(path_to_mov)
# Integrate the clip ID and the timecode into the path as well
grb = [File.basename(path_to_mov).gsub(/\.(.+)$/, ''), OpenSSL::Digest::SHA1.hexdigest(path_to_mov + fractional_timecode.to_s)].join('_')
# Make the tempdirs if needed
FileUtils.mkdir_p(RAILS_ROOT + '/tmp/grabbed_frames')
grabbed_img_path = RAILS_ROOT + "/tmp/grabbed_frames/grab_video_#{grb}%d.jpg"
grabbed_img_path_with_index = grabbed_img_path % 1
# If this frame is already grabbed, just return - we speed up things substantially this way
return grabbed_img_path_with_index if File.exist?(grabbed_img_path_with_index)
# We use succ (next frame) because ffmpeg is 1 frame off
tc = Timecode.from_seconds(fractional_timecode, get_framerate(path_to_mov)).succ.with_fractional_seconds
@logger.warn "Grabbing frame #{tc} from #{path_to_mov}, to #{grabbed_img_path_with_index}"
from_stderr_of("ffmpeg -i #{File.expand_path(path_to_mov)} -an -ss #{tc} -vframes 1 -y #{grabbed_img_path}")
grabbed_img_path_with_index
end
def extract_frame_or_quicktime_logo(fractional_timecode, path_to_mov)
raise_on_nonexistent_file(path_to_mov)
extract_frame(fractional_timecode, path_to_mov)
end
def get_bounds(path_to_mov)
raise_on_nonexistent_file(path_to_mov)
pat = /Video: (.+) (\d+)x(\d+)/
@logger.info "Getting bounds of video stream #{path_to_mov}"
result = from_stderr_of "ffmpeg -i #{File.expand_path(path_to_mov)}"
scan = result.scan(pat)
raise_on_ffmpeg_error(result)
if scan.any?
[scan[0][-2], scan[0][-1]].map(&:to_i)
else
[720, 576]
end
end
def get_framerate(path_to_mov)
raise_on_nonexistent_file(path_to_mov)
result = from_stderr_of("ffmpeg -i #{File.expand_path(path_to_mov)}")
raise_on_ffmpeg_error(result)
movie_framerate = result.scan(/(\d+)(\.(\d+))? tb/).to_s.to_f
raise ParseError, "FPS cannot be zero" if movie_framerate.zero?
movie_framerate
end
private
def raise_on_ffmpeg_error(result)
['could not find codec parameters', 'Unknown format', 'error occured'].each do | bail |
raise MovieIsCorrupted, error_from_ffmpeg_output(result) if result.include?(bail)
end
end
def raise_on_nonexistent_file(path)
raise Error, "The file #{path} is not there" unless File.exist?(path)
end
def error_from_ffmpeg_output(str)
lines = str.split(/\n/)
[lines[-1]].join(" ")
end
def from_stderr_of(cmd)
ffmpeg_check = `which ffmpeg`.to_s
raise Error, "No ffmpeg found on this machine" if (ffmpeg_check.empty? || ffmpeg_check =~ /^no /)
inp, outp, err = Open3.popen3(cmd)
result = err.read.to_s.strip
[inp, outp, err].map{|socket| begin; socket.close; rescue IOError; end }
result
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment