lib/falcon/encoder.rb



require 'fileutils'
require 'web_video'

module Falcon
  module Encoder
    PROCESSING = 1
    SUCCESS = 2
    FAILURE = 3
      
    def self.included(base)
      base.send :include, InstanceMethods
      base.send :extend,  ClassMethods
    end
    
    module ClassMethods
      def self.extended(base)
        base.class_eval do
          belongs_to :videoable, :polymorphic => true
          
          attr_accessor :ffmpeg_resolution, :ffmpeg_padding
          
          attr_accessible :name, :profile_name, :source_path
          
          validates_presence_of :name, :profile_name, :source_path
          
          before_validation :set_resolution
          before_destroy :remove_output
          
          scope :with_profile, lambda {|name| where(:profile_name => Falcon::Profile.detect(name).name) }
          scope :with_name, lambda {|name| where(:name => name) }
          scope :processing, where(:status => PROCESSING)
          scope :success, where(:status => SUCCESS)
          scope :failure, where(:status => FAILURE)
        end
      end
    end
    
    module InstanceMethods
      
      def profile
        @profile ||= Falcon::Profile.find(profile_name)
      end
      
      def resolution
        self.width ? "#{self.width}x#{self.height}" : nil
      end
      
      def transcoder
        @transcoder ||= ::WebVideo::Transcoder.new(source_path)
      end
      
      def output_path
        @output_path ||= self.profile.path(source_path, name).to_s
      end
      
      def output_directory
        @output_directory ||= File.dirname(self.output_path)
      end
      
      def profile_options(input_file, output_file)
        self.profile.encode_options.merge({
          :input_file => input_file,
          :output_file => output_file,
          :resolution => self.ffmpeg_resolution
        })
      end
      
      # A hash of metadatas for video:
      # 
      # { :title => '', :author => '', :copyright => '', 
      #   :comment => '', :description => '', :language => ''}
      #
      def metadata_options
        videoable.send(name).metadata
      end
      
      def encode
        videoable.run_callbacks(:encode) do
          videoable.run_callbacks(:"#{name}_encode") do
            process_encoding
          end
        end
      end
      
      def processing?
        self.status == PROCESSING
      end
      
      def fail?
        self.status == FAILURE
      end
      
      def success?
        self.status == SUCCESS
      end
      
      def processing!
        self.status = PROCESSING
        save(:validate => false)
      end
      
      def fail!
        self.status = FAILURE
        save(:validate => false)
      end
      
      def success!
        self.status = SUCCESS
        self.encoded_at = Time.now
        save(:validate => false)
      end
      
      protected
        
        def process_encoding
          begun_encoding = Time.now

          processing!

          if encode_source && generate_screenshots
            self.encoding_time = (Time.now - begun_encoding).to_i
            success!
          else
            fail!
          end
        end
        
        def set_resolution
          unless profile.nil?
            self.width ||= profile.width
            self.height ||= profile.height
          end
        end
        
        # Calculate resolution and any padding
        def ffmpeg_resolution_and_padding_no_cropping(v_width, v_height)
          in_w = v_width.to_f
          in_h = v_height.to_f
          out_w = self.width.to_f
          out_h = self.height.to_f

          begin
            aspect = in_w / in_h
            aspect_inv = in_h / in_w
          rescue
            Rails.logger.error "Couldn't do w/h to caculate aspect. Just using the output resolution now."
            @ffmpeg_resolution = "#{self.width}x#{self.height}"
            return 
          end

          height = (out_w / aspect.to_f).to_i
          height -= 1 if height % 2 == 1

          @ffmpeg_resolution = "#{self.width}x#{height}"

          # Keep the video's original width if the height
          if height > out_h
            width = (out_h / aspect_inv.to_f).to_i
            width -= 1 if width % 2 == 1

            @ffmpeg_resolution = "#{width}x#{self.height}"
            self.width = width
            self.save(:validate => false)
          # Otherwise letterbox it
          elsif height < out_h
            pad = ((out_h - height.to_f) / 2.0).to_i
            pad -= 1 if pad % 2 == 1
            @ffmpeg_padding = "-vf pad=#{self.width}:#{height + pad}:0:#{pad / 2}"
          end
        end
        
        def encode_source
          stream = transcoder.source.video_stream
          ffmpeg_resolution_and_padding_no_cropping(stream.width, stream.height)
          options = self.profile_options(self.source_path, output_path)
          
          begin
            transcoder.convert(output_path, options) do |command| 
              # Audo 
              command << "-ar $audio_sample_rate$"
              command << "-ab $audio_bitrate_in_bits$"
              command << "-acodec $audio_codec$"
              command << "-ac 1"
              
              # Video 
              command << "-vcodec $video_codec$"
              command << "-b $video_bitrate_in_bits$"
              command << "-bt 240k"
              command << "-r $fps$"
              command << "-f $container$"
              
              # Profile additional arguments
              command << self.profile.command
              
              # Metadata options
              if metadata_options
                metadata_options.each do |key, value|
                  command << "-metadata #{key}=\"#{value}\""
                end
              end
              
              command << self.ffmpeg_padding
              command << "-y"
            end
          rescue ::WebVideo::CommandLineError => e
            ::WebVideo.logger.error("Unable to transcode video #{self.id}: #{e.class} - #{e.message}")
            return false
          end
        end
        
        def generate_screenshots
          image_files = output_path.gsub(File.extname(output_path), '_%2d.jpg')
          options = {:resolution => self.resolution, :count => 1, :at => :center}
          image_transcoder = ::WebVideo::Transcoder.new(output_path) 
          
          begin
            image_transcoder.screenshot(image_files, options) do |command|
              command << "-vcodec mjpeg"
              
              # The duration for which image extraction will take place
              #command << "-t 4"
              command << "-y"
            end
          rescue ::WebVideo::CommandLineError => e
            ::WebVideo.logger.error("Unable to generate screenshots for video #{self.id}: #{e.class} - #{e.message}")
            return false
          end
        end
        
        def remove_output
          FileUtils.rm(output_path, :force => true) if File.exists?(output_path)
        end
    end
  end
end