Created
February 9, 2010 14:10
-
-
Save julik/299222 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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