Migrating from Carrierwave to ActiveStorage in Rails

For the Video Upload Platform series that I have been working on (Sorry I haven’t updated in a bit), I had been using Carrierwave to manage the file uploads. It also has a bunch of plugins/gems available to help with some tasks, however I wanted to move the series more into as “What is actually happening” set of posts. For this reason, I have decided to migrate that project from using Carrierwave to using ActiveStorage, since that has been released somewhat recently and is build into Rails.

If you have been following along in that series, then it should be pretty straight forward using the details I’m about to go into. If not, thats ok too, it should still be pretty familiar to you. At any rate lets get started!

The first thing we want to make sure is that we are update to date with Rails. If you have a specific version set in your Gemfile, remove that, or update it to the latest:

# Gemfile

- gem "rails", "~> 5.1.6
+ gem "rails"

One we have that updated, we just need to bundle the project to pull all the new fun in

bundle

Ok great. Now we want to add the ActiveStorage migrations and configure the set up to use local storage for our example. Do this by running the following command

bundle exec rails active_storage:install
bundle exec rake db:migrate

Great, now we should have our database migrated to contain the ActiveStorage tables. Let create the proper config file that ActiveStorage uses now. Create the file config/storage.yml and paste in the following YAML:

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

This tells ActiveStorage to store things locally. If you are interested in using some type of other storage methods, check out the ActiveStorage Overview page. We only have one more configuration step to go, and we should be good. Add the following line to your config/environments/development.rb file (test.rb and production.rb) as well if you want:

config.active_storage.service = :local

Now that we have ActiveStorage all set up, we can go ahead and update our model to migrate from Carrierwave to ActiveStorage. I will show our model and the line we want to remove and add:

class Video < ApplicationRecord
-  mount_uploader :file, VideoUploader
+  has_one_attached :file
end

As you can see, we no longer are going to use the CarrierwaveUploader. We should now be able to upload files using the same controller and form uploads that we had already created. The last thing we need to change however, is the view. We will pass the url for the upload using some built-in Rails helpers. In the app/views/videos/show.html.erb file, we need to make this change:

<p id="notice"><%= notice %></p>

<p>
  <strong>Title:</strong>
  <%= @video.title %>
</p>

<p>
  <strong>File:</strong>
-  <video controls width="640" height="480" src="<%= @video.file.url %>"></video>
+  <video controls width="640" height="480" src="<%= url_for(@video.file) %>"></video>
</p>

<%= link_to 'Edit', edit_video_path(@video) %> |
<%= link_to 'Back', videos_path %>

The last piece that we need to add in, which I will do in another post, is the ability to do the background Transcoding, that we were getting out of the box with the carrierwave-video gem, but this should get us starting with using ActiveStorage instead of Carrierwave.

Let me know if you have any thoughts in the comments below, or run into any issues. Thanks!

How to create a video upload platform using Ruby on Rails – Part 2

Welcome back to part 2 of How to create a video upload platform using Ruby on Rails. In Part 1, we worked on setting up the basic Ruby on Rails application that allowed us to upload, store, and playback video files from our app. In this next part, we are going to enhance that process some by adding some FFMPEG Transcoding. This way, we can get all videos that are uploaded converted into a common format that will make it easier to work with on the playback side.

So lets get started. The first thing we need to do is make sure we have ffmpeg installed. On mac this is pretty simple using brew. If you don’t have HomeBrew installed, please follow the directions at https://brew.sh/. If you are on another linux or windows system, there are many guides online to help set ffmpeg up for you. Once you have that installed, we can install the ffmpeg command:

brew install ffmpeg

I decided to install a lot of the extra ffmpeg options to make sure I had the best compatibility. Here is a more complete line

brew install ffmpeg --with-chromaprint --with-fdk-aac --with-libass --with-librsvg --with-libsoxr --with-libssh --with-tesseract --with-libvidstab --with-opencore-amr --with-openh264 --with-openjpeg --with-openssl --with-rtmpdump --with-rubberband --with-sdl2 --with-snappy --with-tools --with-webp --with-x265 --with-xz --with-zeromq --with-zimg

Once you have ffmpeg installed and running we need to add the carrierwave-video gem to our project:

# Gemfile
gem "carrierwave-video"

Next, we need to update our upload to include this new library. Open the app/uploaders/video_uploader.rb file and add the following include line to it:

class VideoUploader < CarrierWave::Uploader::Base
 include CarrierWave::Video # <== Add this line here
 
 ...

In order for Carrierwave to actually encode the video, we need to also add a line that instructs it to do so. Lets also adjust the resolution while we are at it.

class VideoUploader < CarrierWave::Uploader::Base
  include CarrierWave::Video

  process encode_video: [:mp4, resolution: "640x480"]

  ...

The last thing we need to do is change the file extension. If we upload an avi or mov file, we convert that to mp4 but with Carrierwave it will maintain the original extension. So add the following lines right after the process line added above

def full_filename(for_file)
  super.chomp(File.extname(super)) + '.mp4'
end

def filename
  original_filename.chomp(File.extname(original_filename)) + '.mp4'
end

Fire up your Rails app and upload a video just like we did in Part 1. You should see the following in your Rails console. Notice the line Running transcoding...

Started POST "/videos" for 127.0.0.1 at 2018-05-17 09:41:10 -0400
Processing by VideosController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"pCgWw3lxulQ9qiGxfck5oqFXgQo0tqO7gG371YEbFaiiRMdAc98uPGQCvTYzn0/Kub/pQtQI9n95I+grmGvQ6w==", "video"=>{"title"=>"adf", "file"=>#<ActionDispatch::Http::UploadedFile:0x007fbb5f814be8 @tempfile=#<Tempfile:/var/folders/__/7ns9m_817l71zfd0xw5618n00000gn/T/RackMultipart20180517-60568-c6cwal.mov>, @original_filename="step.mov", @content_type="video/quicktime", @headers="Content-Disposition: form-data; name=\"video[file]\"; filename=\"step.mov\"\r\nContent-Type: video/quicktime\r\n">}, "commit"=>"Create Video"}
I, [2018-05-17T09:41:10.439284 #60568]  INFO -- : Running transcoding...
["/usr/local/bin/ffmpeg", "-y", "-i", "/Sites/VideoSite/tmp/1526564470-60568-0006-7430/step.mov", "-vcodec", "libx264", "-acodec", "aac", "-s", "640x480", "-r", "30", "-strict", "-2", "-map_metadata", "-1", "-aspect", "1.3333333333333333", "/Sites/VideoSite/tmp/1526564470-60568-0006-7430/tmpfile.mp4"]

I, [2018-05-17T09:41:17.640387 #60568]  INFO -- : Transcoding of /Sites/VideoSite/tmp/1526564470-60568-0006-7430/step.mov to /Sites/VideoSite/tmp/1526564470-60568-0006-7430/tmpfile.mp4 succeeded

   (0.3ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "videos" ("title", "file", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "adf"], ["file", "step.mp4"], ["created_at", "2018-05-17 13:41:17.643006"], ["updated_at", "2018-05-17 13:41:17.643006"]]
   (1.0ms)  commit transaction
Redirected to http://localhost:3000/videos/1
Completed 302 Found in 7260ms (ActiveRecord: 1.6ms)

 

There we go. We are now successfully encoding the video file to a common format using ffmpeg. In the next part we will look at moving this process to the background so that we don’t force the user to wait on the transcoding, and add some status indications of how the progress is going. Until next time!

How to use Delayed Job to handle your Carrierwave processing

This tutorial builds on my previous post about how to add FFMPEG processing to Carrierwave. Here I will show you my attempt at being able to utilize Delayed::Job to do the heavy lifting of processing when uploading files using Carrierwave. Remember, this could probably use some improvement, but it is a great starting point. So lets begin. The first thing you will need to do is add Delayed::Job to your application:

# Gemfile
gem "delayed_job"

Next you need to create the migration and migrate the database:

rails generate delayed_job
rake db:migrate

Now we get to the good part. Lets create a module to include into Carrierwave that will support holding off on doing the processing until Delayed::Job gets around to it:

# lib/carrier_wave/delayed_job.rb
module CarrierWave
  module Delayed
    module Job
      module ActiveRecordInterface
        def delay_carrierwave
          @delay_carrierwave ||= true
        end

        def delay_carrierwave=(delay)
          @delay_carrierwave = delay
        end

        def perform
          asset_name = self.class.uploader_options.keys.first
          self.send(asset_name).versions.each_pair do |key, value|
            value.process_without_delay!
          end
        end

        private

        def enqueue
          ::Delayed::Job.enqueue self
        end
      end

      def self.included(base)
        base.extend ClassMethods
      end

      module ClassMethods
        def self.extended(base)
          base.send(:include, InstanceMethods)
          base.alias_method_chain :process!, :delay
          ::ActiveRecord::Base.send(
            :include,
            CarrierWave::Delayed::Job::ActiveRecordInterface
          )
        end

        module InstanceMethods
          def process_with_delay!(new_file)
            process_without_delay!(new_file) unless model.delay_carrierwave
          end
        end
      end
    end
  end
end

Awesome! Now we need to tie this into our Uploader:

# app/uploaders/asset_uploader.rb
require File.join(Rails.root, "lib", "carrier_wave", "ffmpeg")
require File.join(Rails.root, "lib", "carrier_wave", "delayed_job") # New

class AssetUploader < CarrierWave::Uploader::Base
  include CarrierWave::Delayed::Job # New
  include CarrierWave::FFMPEG

  # Choose what kind of storage to use for this
  uploader: storage :file

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "#{Rails.root}/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Add a version, utilizing our processor
  version :bitrate_128k do
    process :resample => "128k"
  end
end

The last thing we have to do is update our model to queue up delayed job:

# app/models/asset.rb
class Asset < ActiveRecord::Base
  mount_uploader :asset, AssetUploader
  
  after_save :enqueue # New
end

There you have it. Now when you create a new Asset, associate a file, and save it, it shouldn’t run the processes, but instead create a Delayed::Job record. Then Delayed::Job should pick it up and run the processors on it. This may not be perfect, but at least its a start! Thanks for reading!

Create FFMPEG processor for Carrierwave in Rails 3

I have had the pleasure of working with the carrierwave gem recently (as opposed to paperclip), and I must say, I am quite the fan. Once major thing I missed however, was the available list of custom user plugins for it, unlike paperclip. I believe this is mostly due to how new and recent carrierwave is. That being said, I put together a simple example of a FFMPEG process that will allow me to resample the bitrate of a file. This should lay the ground work for other features as well. This example is using Rails 3, but should be easily adaptable for 2. Also, make sure you already have FFMPEG installed and running properly. So lets get started: First things first…we need to add the appropriate gems to our Gemfile:

# Gemfile
gem "carrierwave"
gem "streamio-ffmpeg"

Next is the meat and potatoes of this..the actual FFMPEG process for carrierwave. I choose to keep my plugin files in the directory lib/carrierwave. Make sure you have this path included in your application.rb file if you are using rails 3. Here is the code:

# lib/carrierwave/ffmpeg.rb
require "streamio-ffmpeg"
module CarrierWave
  module FFMPEG
    module ClassMethods
      def resample( bitrate )
        process :resample => bitrate
      end
    end
 
    def resample( bitrate )
      directory = File.dirname( current_path )
      tmpfile = File.join( directory, "tmpfile" )
      File.move( current_path, tmpfile )
      file = ::FFMPEG::Movie.new(tmpfile)
      file.transcode( current_path, :audio_bitrate => bitrate)
      File.delete( tmpfile )
    end
  end
end

Good. Now that we have the plugin coded up, we need to include it into our uploader. I already have one mounted to my Asset model. Here is what my AssetUploader now looks like:

# app/uploaders/asset_uploader.rb
require File.join(Rails.root, "lib", "carrier_wave", "ffmpeg")

class AssetUploader < CarrierWave::Uploader::Base
  include CarrierWave::FFMPEG # <= include the plugin
  
  # Choose what kind of storage to use for this uploader:
  storage :file

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "#{Rails.root}/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
 
  # Add a version, utilizing our processor
  version :bitrate_128k do
    process :resample => "128k"
  end
end

There! Now whenever you add a new file, it should fire off the processor and create a new version. I hope this help anyone still up in the air about how to put together their own plugin/process for carrierwave. Next I will demonstrate how to incorporate Delayed::Job to move these intensive tasks to the background!

%d bloggers like this: