2

I am trying to get uploaded videos to be converted in the background, running windows. Some of what I am using:

gem 'paperclip'
gem 'delayed_job_active_record'
gem 'ffmpeg'

I have edited the registry to allow the ffmpeg command to be ran from anywhere, I get a popup that I assume is ffmpeg because it goes away too quickly, guess the command is wrong so if anyone knows what's wrong with it please let me know. But the real problem is that it just hangs there, it says:

[2012-12-09 22:47:03] ERROR invalid body size.
[2012-12-09 22:47:03] ERROR Errno::ECONNABORTED: An established connection was a
borted by the software in your host machine.
        C:/RailsInstaller/Ruby1.9.3/lib/ruby/1.9.1/webrick/httpresponse.rb:396:i
n `write'
        C:/RailsInstaller/Ruby1.9.3/lib/ruby/1.9.1/webrick/httpresponse.rb:396:i
n `<<'
        C:/RailsInstaller/Ruby1.9.3/lib/ruby/1.9.1/webrick/httpresponse.rb:396:i
n `_write_data'
        C:/RailsInstaller/Ruby1.9.3/lib/ruby/1.9.1/webrick/httpresponse.rb:368:i
n `send_body_string'
        C:/RailsInstaller/Ruby1.9.3/lib/ruby/1.9.1/webrick/httpresponse.rb:249:i
n `send_body'
        C:/RailsInstaller/Ruby1.9.3/lib/ruby/1.9.1/webrick/httpresponse.rb:152:i
n `send_response'
        C:/RailsInstaller/Ruby1.9.3/lib/ruby/1.9.1/webrick/httpserver.rb:110:in
`run'
        C:/RailsInstaller/Ruby1.9.3/lib/ruby/1.9.1/webrick/server.rb:191:in `blo
ck in start_thread'

Does anyone know how to properly get this working? I've went through a few tutorials that have bits and pieces of what I need but I can't get them working together. Here's what I have so far, lemme know if you need more:

Model:

class Video < ActiveRecord::Base

  belongs_to :user
  has_many :comments, dependent: :destroy
  attr_accessible :video, :user_id, :video_file_name, :title, :public, :description, :views

  has_attached_file :video, url: "/users/:user_id/videos/:id/:basename_:style.:extension"

  #process_in_background :video #causes death

  validates :video, presence: true
  validates :description, presence: true, length: { minimum: 5, maximum: 100}
  validates :title, presence: true, length: { minimum: 1, maximum: 15 }

  validates_attachment_size :video, less_than: 1.gigabytes
  validates_attachment :video, presence: true

  default_scope order: 'created_at DESC'

  Paperclip.interpolates :user_id do |attachment, style|attachment.instance.user_id
  end

  #before_post_process do |video|
   # false if video.status == "converting"
  #end

  def perform
    command = <<-end_command
      start ffmpeg -i #{ '/public/users/:user_id/videos/:id/:basename_:style.:extension' }  -ar 22050 -ab 32 -s 1280x720 -vcodec webm -r 25 -qscale 8 -f webm -y #{ '/public/users/:user_id/videos/:id/:basename_.webm' }

    end_command
    success = system(command)
    logger.debug 'Converting File: ' + success.to_s
    if success && $?.exitstatus.to_i == 0
      #self.converted!
      self.status = "converted"
    else
      #self.failure!
      self.status = "failed"
    end
  end

  handle_asynchronously :perform

  def self.search(search)
    if search
      find(:all, conditions: ["public = 't' AND title LIKE ?", "%#{search}%"], order: "created_at DESC")
    else
      find(:all, conditions: ["public = 't'"], order: "created_at DESC")
    end
  end

  def self.admin_search(search)
    if search
      find(:all, conditions: ['title LIKE ?', "%#{search}%"], order: "created_at DESC")
    else
      find(:all, order: "created_at DESC")
    end
  end

  private

    # This updates the stored filename with the new flash video file
    def set_new_filename
      #update_attribute(:filename, "#{filename}.#{id}.webm")
      update_attribute(:content_type, "video/x-webm")
    end

end

Controller:

class VideosController < ApplicationController
    before_filter :signed_in_user, only: [:upload, :update, :destroy]
    before_filter :admin_user, only: :admin_index

    def upload
        @video = Video.new
        # generate a unique id for the upload
        @uuid = (0..29).to_a.map {|x| rand(10)}
    end

    def create
        @video = Video.new(params[:video])
        @video.user_id = current_user.id

        if @video.save
            @video.delay.perform
            flash[:success] = "Uploaded Succefully!"
            redirect_to @video.user
            Delayed::Worker.new.start
        else
            render 'upload'
        end
    end

    def show
        @video = Video.find(params[:id])
        @comments = @video.comments.paginate(page: params[:page], per_page: 6)
        if !@video.public
            if !signed_in? || current_user.id != @video.user_id  && !current_user.admin && !current_user.approved?(@video.user)
            flash[:notice] = "Video is private"
            redirect_to root_path
        end
    end
    end

    def update
        @video = Video.find(params[:id])
        if @video.update_attributes(params[:video])
      flash[:success] = "Video preferences saved"
    else
        flash[:fail] = "Failed to update video preferences"
    end
    redirect_to :back
  end

    def destroy
        @video = Video.find(params[:id])
        @video.destroy
        flash[:deleted] = "Deleted Succefully!"
        redirect_to :back
    end

    def index
        @videos = Video.paginate(page: params[:page], per_page: 6).search(params[:search])
    end

    def admin_index
        @videos = Video.paginate(page: params[:page], per_page: 6).admin_search(params[:search])
    end

    def ajax_video_comments
        @video = Video.find(params[:id])
        @comments = @video.comments.paginate(page: params[:page], per_page: 6)

        respond_to do |format|
        format.js   { render partial: 'shared/comments', content_type: 'text/html' }
    end
    end

    def ajax_video_watched
        @video = Video.find(params[:id])
        @video.views += 1
        @video.save
    end

    private

    def signed_in_user
        redirect_to root_path, notice: "Please Login." unless signed_in?
    end

    def admin_user
        redirect_to(root_path) unless current_user.admin?
    end

end
Matthew
  • 35
  • 1
  • 8
  • Can you try without the delay? (success = system(convert_command)). I guess the delayed job avoids creating the expected Process::Status object (referenced by $?). – alto Dec 04 '12 at 23:26
  • @alto Ok, ya that allows it to at least load to the user videos page, but it still doesn't convert and it puts error into the aasm_state field instead of converting, which never changes – Matthew Dec 05 '12 at 03:35
  • What's in the aasm_state field? There is no reference to AASM in your example code at all. – alto Dec 15 '12 at 09:12
  • @alto Sorry, it's been removed since in exchange for a string field called status that I set and control manually. That you can see in the code – Matthew Dec 15 '12 at 14:09
  • Anyone have any suggestions at all? Open to try different things... still has not been solved – Matthew Jan 13 '13 at 04:49
  • Looks to me like your video is larger than what would be your "client_max_body_size". I have done what your are trying to do but I went about it a bit differently. That said what you have looks okay at first glance. I bet if you run a test with a smaller video the connection won't drop. – Larry McKenzie Mar 05 '13 at 19:48
  • @LAMCreations Actually, I've been trying very small videos so I don't believe it's that... any other suggestions? Though I didn't get any error messages this time I tried, just got stuck... – Matthew Mar 06 '13 at 00:03
  • @DragonFire353 I found this thread because I was hunting for a solution to a problem I have been having with paperclip v3.4.1 and the aws-sdk > v1.5.0. If you are using aws-sdk with paperclip I rolled back to paperclip v3.0.4 to fix a similar error. As far as how your using paperclip I can post a working sample model and processor if you would like but I don't know if it will help. – Larry McKenzie Mar 06 '13 at 01:59
  • @LAMCreations I'm not using aws, the processor could possibly help if you don't mind – Matthew Mar 06 '13 at 21:29
  • @DragonFire353 I posted the code as an answer but I'm curious what version of paperclip your using? – Larry McKenzie Mar 09 '13 at 02:44
  • @DragonFire353 also no need for the ffmpeg gem with my solution. You can use any ffmpeg option in paperclip styles. I can also provide a solution to outsource the video processing to another service. – Larry McKenzie Mar 09 '13 at 02:46

2 Answers2

3

Your table should have these columns:

  • video_file_name
  • video_content_type
  • video_file_size
  • video_updated_at
  • video_meta

I added some additional paperclip magic to you model, obviously you can tweak the settings for ffmpeg. This is not all original code but I can't remember where I found bits and pieces so if someone recognizes it feel free to take credit.

class Video < ActiveRecord::Base

  belongs_to :user
  has_many :comments, dependent: :destroy
  attr_accessible :video, :user_id, :video_file_name, 
                  :title, :public, :description, :views

  has_attached_file :video, 
    url: "/users/:user_id/videos/:id/:basename_:style.:extension"
    styles: {
             :original => { :geometry => "1280x720", :format => 'mp4', :streaming => true, :convert_options => { :input => {}, :output => {'c:v' => 'libx264', vprofile: 'high', preset: 'medium', 'b:v' => '1250k', maxrate: '1250k', bufsize: '2500k', pix_fmt: 'yuv420p', flags: '+mv4+aic', threads: 'auto', 'b:a' => '128k', strict: '-2'}} },
             :medium => { :geometry => "854x480", :format => 'mp4', :streaming => true,                 :convert_options => { :input => {}, :output => {'c:v' => 'libx264', vprofile: 'high', preset: 'medium', 'b:v' => '750k', maxrate: '750k', bufsize: '1500k', pix_fmt: 'yuv420p', flags: '+mv4+aic', threads: 'auto', 'b:a' => '128k', strict: '-2'}} },
             :small => { :geometry => '640x360', :format => 'mp4', :streaming => true, :convert_options => { :input => {}, :output => {'c:v' => 'libx264', vprofile: 'high', preset: 'medium', 'b:v' => '250k', maxrate: '250k', bufsize: '500k', pix_fmt: 'yuv420p', flags: '+mv4+aic', threads: 'auto', 'b:a' => '128k', strict: '-2'}} },
             :thumb => { :geometry => "160x90", :format => 'jpg', :time => 10 }
            }, 
    processors: [:ffmpeg, :qtfaststart]

  validates :video, presence: true
  validates :description, presence: true, length: { minimum: 5, maximum: 100}
  validates :title, presence: true, length: { minimum: 1, maximum: 15 }

  validates_attachment_size :video, less_than: 1.gigabytes
  validates_attachment :video, presence: true

  default_scope order: 'created_at DESC'

  # cancel post-processing now, and set flag...


  before_post_process do |video|
    if video.status == nil
      video.status = "queuing"
      false # halts processing
    end
  end

  # ...and perform after save in background
  after_commit do |video| 
    if video.status == "queuing"
      Delayed::Job.enqueue VideoJob.new(video.id), :queue => 'video'
      video.status == "queued"
      video.save(validations: false)
    end
  end

  # generate styles (downloads original first)
  def regenerate_styles!
    self.video.reprocess!
  end

  # detect if our source file has changed
  def video_changed?
    self.video_file_size_changed? || 
    self.video_file_name_changed? ||
    self.video_content_type_changed? || 
    self.video_updated_at_changed?
  end

  # Class to perform with delayed jobs
  class VideoJob < Struct.new(:video_id)

    def perform
      video = Video.find(self.video_id)
      video.status = "processing"
      video.save(validations: false)
      video.regenerate_styles!
    end

    def success(job)
      video = Video.find(self.video_id)
      video.status = "complete"   
      video.save(:validate => false)
    end

    def error(job, exception)
      video = Video.find(self.video_id)
      video.status = "error"   
      video.save(:validate => false)
    end
  end
end

Paperclip Processors (/lib/paperclip_processors/ffmpeg.rb):

module Paperclip
  class Ffmpeg < Processor
    attr_accessor :geometry, :format, :whiny, :convert_options

    # Creates a Video object set to work on the +file+ given. It
    # will attempt to transcode the video into one defined by +target_geometry+
    # which is a "WxH"-style string. +format+ should be specified.
    # Video transcoding will raise no errors unless
    # +whiny+ is true (which it is, by default. If +convert_options+ is
    # set, the options will be appended to the convert command upon video transcoding.
    def initialize file, options = {}, attachment = nil
      @convert_options = {
        :input => {},
        :output => { :y => nil }
      }
      unless options[:convert_options].nil? || options[:convert_options].class != Hash
        unless options[:convert_options][:input].nil? || options[:convert_options][:input].class != Hash
          @convert_options[:input].reverse_merge! options[:convert_options][:input]
        end
        unless options[:convert_options][:output].nil? || options[:convert_options][:output].class != Hash
          @convert_options[:output].reverse_merge! options[:convert_options][:output]
        end
      end

      @geometry        = options[:geometry]
      @file            = file
      @keep_aspect     = !@geometry.nil? && @geometry[-1,1] != '!'
      @pad_only        = @keep_aspect    && @geometry[-1,1] == '#'
      @enlarge_only    = @keep_aspect    && @geometry[-1,1] == '<'
      @shrink_only     = @keep_aspect    && @geometry[-1,1] == '>'
      @whiny           = options[:whiny].nil? ? true : options[:whiny]
      @format          = options[:format]
      @time            = options[:time].nil? ? 3 : options[:time]
      @current_format  = File.extname(@file.path)
      @basename        = File.basename(@file.path, @current_format)
      @meta            = identify
      @pad_color       = options[:pad_color].nil? ? "black" : options[:pad_color]
      attachment.instance_write(:meta, @meta)
    end
    # Performs the transcoding of the +file+ into a thumbnail/video. Returns the Tempfile
    # that contains the new image/video.
    def make
      src = @file
      dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
      dst.binmode

      parameters = []
      # Add geometry
      if @geometry
        # Extract target dimensions
        if @geometry =~ /(\d*)x(\d*)/
          target_width = $1
          target_height = $2
        end
        # Only calculate target dimensions if we have current dimensions
        unless @meta[:size].nil?
          current_geometry = @meta[:size].split('x')
          # Current width and height
          current_width = current_geometry[0]
          current_height = current_geometry[1]
          if @keep_aspect
            if @enlarge_only
              if current_width.to_i < target_width.to_i
                # Keep aspect ratio
                width = target_width.to_i
                height = (width.to_f / (@meta[:aspect].to_f)).to_i
                @convert_options[:output][:s] = "#{width.to_i/2*2}x#{height.to_i/2*2}"
              else
                return nil
              end
            elsif @shrink_only
              if current_width.to_i > target_width.to_i
                # Keep aspect ratio
                width = target_width.to_i
                height = (width.to_f / (@meta[:aspect].to_f)).to_i
                @convert_options[:output][:s] = "#{width.to_i/2*2}x#{height.to_i/2*2}"
              else
                return nil
              end
            elsif @pad_only
              # Keep aspect ratio
              width = target_width.to_i
              height = (width.to_f / (@meta[:aspect].to_f)).to_i
              # We should add half the delta as a padding offset Y
              pad_y = (target_height.to_f - height.to_f) / 2
              if pad_y > 0
                @convert_options[:output][:vf] = "scale=#{width}:-1,pad=#{width.to_i}:#{target_height.to_i}:0:#{pad_y}:#@pad_color"
              else
                @convert_options[:output][:vf] = "scale=#{width}:-1,crop=#{width.to_i}:#{height.to_i}"
              end
            else
              # Keep aspect ratio
              width = target_width.to_i
              height = (width.to_f / (@meta[:aspect].to_f)).to_i
              @convert_options[:output][:s] = "#{width.to_i/2*2}x#{height.to_i/2*2}"
            end
          else
            # Do not keep aspect ratio
            @convert_options[:output][:s] = "#{target_width.to_i/2*2}x#{target_height.to_i/2*2}"
          end
        end
      end
      # Add format
      case @format
      when 'jpg', 'jpeg', 'png', 'gif' # Images
        @convert_options[:input][:ss] = @time
        @convert_options[:output][:vframes] = 1
        @convert_options[:output][:f] = 'image2'
      end

      # Add source
      parameters << @convert_options[:input].map { |k,v| "-#{k.to_s} #{v} "}
      parameters << "-i ':source'"
      parameters << @convert_options[:output].map { |k,v| "-#{k.to_s} #{v} "}
      parameters << "':dest'"

      parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")

      begin
        success = Paperclip.run("ffmpeg", parameters, :source => "#{File.expand_path(src.path)}", :dest => File.expand_path(dst.path))
      rescue Cocaine::ExitStatusError => e
        raise Paperclip::Error, "error while processing video for #{@basename}: #{e}" if @whiny
      end

      dst
    end

    def identify
      meta = {}
      command = "ffmpeg -i \"#{File.expand_path(@file.path)}\" 2>&1"
      ffmpeg = IO.popen(command)
      ffmpeg.each("\r") do |line|
        # Matching lines like:
        # Video: h264, yuvj420p, 640x480 [PAR 72:72 DAR 4:3], 10301 kb/s, 30 fps, 30 tbr, 600 tbn, 600 tbc
        if line.include?(' Video: ')
          start = line.index('Video:')
          items = line[start, 150].split(',')
          size = items[2].strip!.split(' ').first
          meta[:size] = size.to_s
          meta[:aspect] = size.split('x').first.to_f / size.split('x').last.to_f
        end
        # Matching Duration: 00:01:31.66, start: 0.000000, bitrate: 10404 kb/s
        if line =~ /Duration:(\s.?(\d*):(\d*):(\d*\.\d*))/
          meta[:length] = $2.to_s + ":" + $3.to_s + ":" + $4.to_s
        end
      end
      meta
    end
  end

  class Attachment
    def meta
      instance_read(:meta)
    end
  end
end

Paperclip Processors (/lib/paperclip_processors/qtfaststart.rb):

module Paperclip
  class Qtfaststart < Processor
    attr_accessor :streaming, :format, :whiny

    # Creates a Video object set to work on the +file+ given. It
    # will attempt to reposition the moov atom in the video given
    # if +streaming+ is set.
    def initialize file, options = {}, attachment = nil
      @streaming      = options[:streaming]
      @file            = file
      @whiny           = options[:whiny].nil? ? true : options[:whiny]
      @format          = options[:format]
      @current_format  = File.extname(@file.path)
      @basename        = File.basename(@file.path, @current_format)
      @meta            = attachment.meta
      attachment.instance_write(:meta, @meta)
    end

    # Performs the atom repositioning on +file+.
    # Returns the Tempfile that contains the new video or the original
    # file if +streaming+ wasn't set. 
    def make
      return @file unless @streaming

      src = @file
      dst = Tempfile.new([@basename, @format ? ".#{@format}" : ''])
      dst.binmode

      parameters = []
      # Add source
      parameters << ":source"
      # Add destination
      parameters << ":dest"

      parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")

      Paperclip.log("[qtfaststart] #{parameters}")
      begin
        success = Paperclip.run("qt-faststart", parameters, :source => "#{File.expand_path(src.path)}", :dest => File.expand_path(dst.path))
      rescue Cocaine::ExitStatusError => e
        raise PaperclipError, "error while processing video for #{@basename}: #{e}" if @whiny
      end
      dst
    end
  end

  class Attachment
    def meta
      instance_read(:meta)
    end
  end
end

You controller could use some more cleaning but I just made adjustments to make it work with my other changes.

class VideosController < ApplicationController
    before_filter :signed_in_user, only: [:upload, :update, :destroy]
    before_filter :admin_user, only: :admin_index

    def upload
        @video = Video.new
        # generate a unique id for the upload
        @uuid = (0..29).to_a.map {|x| rand(10)}
    end

    def create
        @video = Video.new(params[:video])
        @video.user_id = current_user.id

        if @video.save
            flash[:success] = "Uploaded Succefully!"
            redirect_to @video.user
        else
            render 'upload'
        end
    end

    def show
        @video = Video.find(params[:id])
        @comments = @video.comments.paginate(page: params[:page], per_page: 6)
        if !@video.public
            if !signed_in? || current_user.id != @video.user_id  && !current_user.admin && !current_user.approved?(@video.user)
            flash[:notice] = "Video is private"
            redirect_to root_path
        end
    end
    end

    def update
        @video = Video.find(params[:id])
        if @video.update_attributes(params[:video])
      flash[:success] = "Video preferences saved"
    else
        flash[:fail] = "Failed to update video preferences"
    end
    redirect_to :back
  end

    def destroy
        @video = Video.find(params[:id])
        @video.destroy
        flash[:deleted] = "Deleted Succefully!"
        redirect_to :back
    end

    def index
        @videos = Video.paginate(page: params[:page], per_page: 6).search(params[:search])
    end

    def admin_index
        @videos = Video.paginate(page: params[:page], per_page: 6).admin_search(params[:search])
    end

    def ajax_video_comments
        @video = Video.find(params[:id])
        @comments = @video.comments.paginate(page: params[:page], per_page: 6)

        respond_to do |format|
        format.js   { render partial: 'shared/comments', content_type: 'text/html' }
    end
    end

    def ajax_video_watched
        @video = Video.find(params[:id])
        @video.views += 1
        @video.save
    end

    private

    def signed_in_user
        redirect_to root_path, notice: "Please Login." unless signed_in?
    end

    def admin_user
        redirect_to(root_path) unless current_user.admin?
    end

end

You can run a delayed_jobs worker thread how ever works best for you. I may have made some mistakes in here but I tried to adapt my method to your current model.

Larry McKenzie
  • 3,253
  • 25
  • 22
1

Bit late, but we use the paperclip-ffmeg gem - something you might want to check out

All you need to do is put them gem into your gemfile, and then you just have to define the processor as :ffmpeg.

Here's a live example from our code:

class Attachment < ActiveRecord::Base

        has_attached_file :attachment,
                styles:          lambda { |a| a.instance.is_image? ? {:small => "x200>", :medium => "x300>", :large => "x400>"}  : {:thumb => { :geometry => "100x100#", :format => 'jpg', :time => 10}, :medium => { :geometry => "300x300#", :format => 'jpg', :time => 10}}},
                :processors => lambda { |a| a.is_video? ? [ :ffmpeg ] : [ :thumbnail ] }

        def is_video?
                attachment.instance.attachment_content_type =~ %r(video)
        end

        def is_image?
                attachment.instance.attachment_content_type =~ %r(image)
        end

end
Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • I believe I did try this but it didn't solve the issue with it not processing in the background after a successful upload, causing the user to wait until it has finished before it uploads – Matthew Oct 24 '13 at 16:28