module Bulldog
module Attachment
class Video < Base
handle :video
include HasDimensions
#
# Return the duration of the named style, as an
# ActiveSupport::Duration.
#
# This runs ffmpeg for, and only for, the original style.
#
# +style_name+ defaults to the attribute's #default_style.
#
def duration(style_name)
from_examination :@original_duration
end
#
# Return the video tracks of the named style, as an array of
# VideoTrack objects.
#
# Each VideoTrack has:
#
# * #duration - the duration of the video track.
# * #dimensions - the [width, height] of the video
# track.
#
def video_tracks(style_name=nil)
style_name ||= reflection.default_style
if style_name.equal?(:original)
from_examination :@original_video_tracks
else
style = reflection.styles[style_name]
video_tracks(:original).map do |video_track|
dimensions = resize_dimensions(dimensions(:original), style)
VideoTrack.new(:dimensions => dimensions)
end
end
end
#
# Return the video tracks of the named style, as an array of
# AudioTrack objects.
#
# AudioTrack objects do not yet have any useful methods.
#
def audio_tracks(style_name=nil)
examine
@original_audio_tracks
end
storable_attribute :duration, :per_style => true, :memoize => true
def unload
super
instance_variables.grep(/@original_/).each do |name|
instance_variable_set(name, nil)
end
end
protected # ---------------------------------------------------
#
# Return the default processor class to use for this attachment.
#
def default_processor_type
:ffmpeg
end
#
# Overridden to round down to multiples of 2, as required by
# some codecs.
#
def resize_dimensions(original_dimensions, style)
dimensions = super
dimensions.map!{|i| i &= -2}
end
private # -----------------------------------------------------
#
# Read the original image metadata with ffmpeg.
#
def run_examination
if stream.missing?
set_defaults
false
else
output = `ffmpeg -i #{stream.path} 2>&1`
# ffmpeg exits nonzero - don't bother checking status.
parse_output(output)
true
end
end
def set_defaults
@original_duration = 0
@original_width = 2
@original_height = 2
@original_audio_tracks = []
@original_video_tracks = []
end
def parse_output(output)
result = false
set_defaults
io = StringIO.new(output)
while (line = io.gets)
case line
when /^Input #0, (.*?), from '(?:.*)':$/
result = true
duration = nil
when /^ Duration: (\d+):(\d+):(\d+)\.(\d+)/
duration = $1.to_i.hours + $2.to_i.minutes + $3.to_i.seconds
when /Stream #(?:.*?): Video: /
if $' =~ /(\d+)x(\d+)/
dimensions = [$1.to_i, $2.to_i]
end
@original_video_tracks << VideoTrack.new(:dimensions => dimensions, :duration => duration)
when /Stream #(?:.*?): Audio: (.*?)/
@original_audio_tracks << AudioTrack.new(:duration => duration)
end
end
if (track = @original_video_tracks.first)
@original_width, @original_height = *track.dimensions
end
if (track = @original_video_tracks.first || @original_audio_tracks.first)
@original_duration = track.duration
end
result
end
class Track
def initialize(attributes={})
attributes.each do |name, value|
send("#{name}=", value)
end
end
attr_accessor :duration
end
class VideoTrack < Track
attr_accessor :dimensions
end
class AudioTrack < Track
end
end
end
end