Ruby
Article

Video Uploads with Rails and Ziggeo

By Ilya Bodrov-Krukowski

ziggeo

I remember times when virtually no one on the Internet actually watched videos because the connection speed was too slow. Then, the speed started to grow and I could download music tracks: it took roughly 4-5 minutes to download a single track while I was listening to another one. In 2005 YouTube emerged and video content started to spread. Nowadays videos are everywhere and many people actually prefer them over text (though there are many others who like to read the text instead).

I’ve already covered the process of working with the YouTube API in one of my previous articles, so today we will discuss another video hosting platform called Ziggeo. It provides an API to store and manage videos along with the ability to embed them (using a simple player). Also, there are additional features like comments, moderation, callbacks access rights, integrations with third-party services, and more. Ziggeo has both free and paid pricing plans. With the free plan, you can upload videos with the total length of 100 minutes, so it’s a great option for testing the service.

In this article I will show you how to upload videos to Ziggeo, associate them with a user, embed them, fetch meta information, setup server callbacks, and listen to events.

The source code is available on GitHub.

Special thanks to Ziggeo’s support team who provided quick and professional help.

Creating an Application

As usual, start off by creating a new Rails 5 application called ZigZag:

$ rails new ZigZag -T

Drop in some necessary gems:

Gemfile

# ...
gem 'Ziggeo'
gem 'dotenv-rails'
gem 'devise'
  • Ziggeo is a gem allowing you to easily work with the Ziggeo’s API. Note that its name starts with an uppercase “Z”!
  • Dotenv will be used to store environment variables for development.
  • Devise will provide an authentication solution.

Install these gems, create a basic configuration for Devise, and add a User model:

$ bundle install
$ rails generate devise:install
$ rails generate devise User
$ rails db:migrate

Create a VideosController and set up some routes:

videos_controller.rb

class VideosController < ApplicationController
end

config/routes.rb

# ...
resources :videos, only: [:new, :index, :destroy]
root 'videos#index'

Enforce user authentication inside the ApplicationController:

application_controller.rb

# ...
before_action :authenticate_user!

Also, let’s display flash messages inside the layout:

views/layouts/application.html.erb

# ...
<% flash.each do |key, value| %>
  <div class="alert alert-<%= key %>">
    <%= value %>
  </div>
<% end %>

That’s it. The next step is setting up Ziggeo.

Ziggeo Initial Setup

Sign up for Ziggeo before proceeding. Once you are done, an application with the name “Default” will be available for you, but, of course, another one may be created. Each application has its own settings, associated videos, and keys to work within the API. Ziggeo provides three keys, so place them inside the .env file:

.env

ZIGGEO_KEY=123
ZIGGEO_SECRET=abc
ZIGGEO_ENCRYPTION=345

Make sure you exclude this file from Git:

.gitignore

.env

Ziggeo also provides a nice quick start guide so you may want to browse it, as well. Next, include the necessary CSS and JavaScript files:

views/layouts/application.html.erb

<link rel="stylesheet" href="//assets-cdn.ziggeo.com/v1-stable/ziggeo.css" />
<script src="//assets-cdn.ziggeo.com/v1-stable/ziggeo.js"></script>
<script>ZiggeoApi.token = '<%= ENV['ZIGGEO_KEY'] %>';</script>

Basically, this is enough to start uploading and embedding videos on the website.

Uploading and Recording

Ziggeo instructs us to place a special tag (ziggeo) on the page. It acts as a video player, video recorder, and video uploader at the same time. To control the component’s behavior and appearance, multiple options can be set.

Here is the simplest variant:

views/videos/new.html.erb

<ziggeo ziggeo-width="320" ziggeo-height="240"></ziggeo>

This will create a special control to record and upload videos. I do not want videos to be too long, so set a ziggeo-limit parameter:

<ziggeo ziggeo-width="320" ziggeo-height="240" ziggeo-limit="60"></ziggeo>

60 here means “no more than 60 seconds”. Also, it would be nice to allow user to upload already recorded videos, so set ziggeo-perms to allowupload:

<ziggeo ziggeo-limit="60"
        ziggeo-width="320"
        ziggeo-height="240"
        ziggeo-perms="allowupload"></ziggeo>

Now you may test it out. Any uploaded video will appear inside the “Videos” and “Moderation” tabs on Ziggeo’s dashboard. Apart from the actual video file, you will see additional meta information, like its length, size, creation date, tags, and more. Also, there is a handy “Events” section listing all the events that have happened inside your application.

Embedding Videos

Use the same ziggeo tag to embed videos on the page. The main attribute is the ziggeo-video that accepts a video’s uid:

<ziggeo ziggeo-video='123abc'
        ziggeo-width="320"
        ziggeo-height="240">
</ziggeo>

What’s more, videos can be played in a popup. To do this, simply set the ziggeo-popup argument:

<ziggeo ziggeo-video='123abc'
        ziggeo-width="320"
        ziggeo-height="240" ziggeo-popup>
</ziggeo>

There is a sandbox available where you can see uploading and embedding in action as well as test various configuration options.

Also note that that Ziggeo’s player has some styling applied, but it does not look very nifty. Therefore, you may further style it as needed. Here is the playground to see styling in action.

Listening to Events

Ziggeo provides a whole bunch of Javascript events that you can listen to: play, pause, uploaded, and camera_nosignal to name a few. For example, let’s create a custom progress bar showing how the video upload process is going. This is easy to do with the upload_progress event that constantly fires saying how many bytes have been uploaded so far.

First of all, add the progress element to the page. I am using Bootstrap 4 to style it, but you may use any other CSS framework or write your own styles:

views/videos/new.html.erb

<!-- ... -->
<progress class="progress progress-striped hidden-xs-up" value="0" max="100"></progress>

The hidden-xs-up class will hide this progress bar on all screens. Alternatively, you may simply say display: none.

Now write some CoffeeScript code:

javascripts/videos.coffee

jQuery(document).on 'turbolinks:load', ->
  ZiggeoApi.Events.on "upload_progress", ( uploaded, total, data ) ->
    $('progress').removeClass('hidden-xs-up').attr 'value', (uploaded / total) * 100

ZiggeoApi is the global object available for us after the Ziggeo’s JS is loaded. The uploaded and total variables contains the number of bytes, so dividing them and multiplying by 100 gives us a percentage. The data variable contains information about the video that’s currently being uploaded.

Don’t forget to include this new file:

javascript/application.js

//= require videos

Another thing we may do is redirect the user to the root page after a video is uploaded:

javascripts/videos.coffee

ZiggeoApi.Events.on "submitted", ( data ) -> window.location.href = '/'

Simple, isn’t it?

Also note that Ziggeo provides some callable methods to manipulate the player and the uploader. These methods can be found in the docs.

Querying the API

So, now the videos can be uploaded, but we don’t display them anywhere. In order to fetch a list of uploaded videos, you may utilize the Ziggeo gem that we’ve included at the beginning of the article.

Working with it is very simple. You just need to instantiate the client by passing all three keys obtained earlier and then call the required method:

videos_controller.rb

# ...
def index
  ziggeo = Ziggeo.new(ENV['ZIGGEO_KEY'], ENV['ZIGGEO_SECRET'], ENV['ZIGGEO_ENCRYPTION'])
  @videos = ziggeo.videos.index
end

The index method accepts arguments like limit and skip to further customize the request. As a result, the @videos variable will contain an array of hashes, each of which store the video’s uid and other meta information. This array can now be rendered on the main page:

views/videos/index.html.erb

<h1>Videos</h1>

<%= link_to 'Add video', new_video_path %>

<%= render partial: 'video', collection: @videos, as: :video %>

Note that we can’t say render @videos as it’s a simple array. Now render the player inside the partial. Videos will be opened in a popup:

views/videos/_video.html.erb

<div class="card">
  <div class="card-block">
    <ziggeo ziggeo-video='<%= video['token'] %>'
            ziggeo-width="320"
            ziggeo-height="240" ziggeo-popup>
    </ziggeo>
</div>

The token field contains the video’s unique identifier.

Associating Videos With Users

One common piece of functionality is allowing each user to have their own videos. Currently, however, videos have no information about who owns them. Unfortunately, there is no special field to store this information, but it can be solved by using the video’s tags. Tags are passed in a form of a comma-delimited string that is later turned into an array. When fetching the videos later, we will simply pass the tags option.

We might utilize user ids as tags, but that’s not very secure. Instead, lets generate a unique token after a user is created:

models/user.rb

# ...
before_create -> { self.uid = generate_uid }

private

def generate_uid
  loop do
    uid = Digest::MD5.hexdigest(self.email + self.created_at.to_s + rand(10000).to_s)
    return uid unless User.exists?(uid: uid)
  end
end

We simply generate an MD5 hash based on the user’s email, creation date, and some random number. This token must be unique, so we make sure no other user already has it.

The uid field does not exist yet, so create the corresponding migration now:

$ rails g migration add_uid_to_users uid:string

Tweak the migration to include an index enforcing uniqueness:

db/migrate/xyz_add_uid_to_users.rb

class AddUidToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :uid, :string
    add_index :users, :uid, unique: true
  end
end

Now apply the migration:

$ rails db:migrate

Having this uid in place, we can set the token for the videos. This is as simple as providing the ziggeo-tags argument. Remember that it accepts a comma-separated string:

views/videos/new.html.erb

<ziggeo ziggeo-limit="60"
        ziggeo-width="320"
        ziggeo-height="240"
        ziggeo-perms="allowupload"
        ziggeo-tags="<%= current_user.uid %>"></ziggeo>

Great! Now in order to fetch only the current user’s videos, set the tags option for the index method:

videos_controller.rb

# ...
def index
  ziggeo = Ziggeo.new(ENV['ZIGGEO_KEY'], ENV['ZIGGEO_SECRET'], ENV['ZIGGEO_ENCRYPTION'])
  @videos = ziggeo.videos.index(tags: current_user.uid)
end

Nice, but we can do better. The problem with Ziggeo’s API is that there is no way to fetch only the videos that are approved by the moderator. That’s strange, but Ziggeo’s support team confirmed this information. Of course, we can filter the approved videos in our controller by using the keep_if method, but then if you’d like to employ a pagination mechanism, things will become pretty complex. Therefore, why don’t we set up server callbacks and store the videos’ information in our own database the way we see fit? Let’s do it in the next section!

Setting Up Callbacks

Preparations and Video Creation

Callbacks are configured per-application, so open your dashboard, choose an application, and click Manage > Web Hooks. Here type in a URL (I’ll go with https://sitepoint-ziggeo.herokuapp.com/api/video_callbacks) and choose “JSON encoding” from the dropdown. Now events will be forwarded to /api/video_callbacks in the form of a POST request. Here is the list of all callbacks that you can use. Note that not all events are forwarded – only the most important ones.

First of all, we want to track the addition of all videos, therefore a new model called Video will be required. It is going to contain the following fields:

  • uid (string, indexed, unique) – the video’s token
  • user_id (integer, indexed) – foreign key to establish the relation between a user and a video
  • duration (decimal) – the video’s duration in seconds
  • ziggeo_created_at (datetime) – date and time when the video was created on Ziggeo
  • approved (boolean, indexed) – whether a video was approved by moderator, default is false

Create the corresponding migration:

$ rails g model Video user:belongs_to uid:string duration:decimal ziggeo_created_at:datetime approved:boolean

Tweak the migration a bit:

db/migrate/xyz/create_videos.rb

# ...
create_table :videos do |t|
  t.string :uid
  t.belongs_to :user, foreign_key: true
  t.decimal :duration, scale: 2, precision: 5
  t.datetime :ziggeo_created_at
  t.boolean :approved, default: false

  t.timestamps
end

add_index :videos, :approved
add_index :videos, :uid, unique: true

Apply it:

$ rails db:migrate

Make sure the proper associations and validations are set:

models/user.rb

# ...
has_many :videos, dependent: :destroy

models/video.rb

# ...
belongs_to :user
validates :uid, presence: true, uniqueness: true

Now add a new route namespaced under :api:

config/routes.rb

namespace :api do
  resources :video_callbacks, only: [:create]
end

Create a new controller inside the api folder:

controllers/api/video_callbacks_controller.rb

class Api::VideoCallbacksController < ActionController::Base
  def create
  end
end

When a new event arrives, it has an event_type param set to some value. Currently we’ll be interested in the video_ready event. Lets just take the video’s data and create a new record based on it:

controllers/api/video_callbacks_controller.rb

class Api::VideoCallbacksController < ActionController::Base
  def create
    type = params['event_type']
        respond_to do |format|
          @result = if type == 'video_ready'
                      Video.from_api(params['data']['video'])
                    end
        end
  end
end

Video’s data is stored under the ['data']['video'] key.

Also respond with 204 (no content) status code if everything is okay, or with 500 (server error) if something has gone wrong:

controllers/api/video_callbacks_controller.rb

def create
  type = params['event_type']
  respond_to do |format|
    @result = if type == 'video_ready'
                Video.from_api(params['data']['video'])
              end
    format.html { @result ? head(:no_content) : head(500) }
  end
end

Now code the from_api class method. It should fetch a user based on the video’s tag (remember that we are utilizing the user’s UID as a tag) and create a new record that belongs to him:

models/video.rb

# ...
def self.from_api(data)
  user = User.find_by(uid: data['tags'][0])
  video = user.videos.find_or_initialize_by(uid: data['token'])
  video.ziggeo_created_at = Time.at(data['created'])
  video.duration = data['duration']
  video.save
end

tags contain an array, so we simply grab the first element. I’ve noticed that sometimes an event may be sent twice, so use find_or_initialize_by to avoid creation of duplicate records. Well, this won’t be possible as the index set for the uid enforces uniqueness, but still.

Video Approval

When a video is approved or rejected by a moderator, the corresponding event is sent as well. We will work with the video_approve event type. When it arrives, find the video in the database based on its token (uid) and set the approved attribute to true:

controllers/api/video_callbacks_controller.rb

def create
  type = params['event_type']
  respond_to do |format|
    @result = if type == 'video_ready'
                Video.from_api(params['data']['video'])
              else
                if type == 'video_approve'
                  video = Video.find_by(uid: params['data']['video']['token'])
                  video.approve! if video
                else
                  true
                end
              end
    format.html { @result ? head(:no_content) : head(500) }
  end
end

We simply assign true to the @result instance variable if the event is some other type. Here is the approve! method:

models/video.rb

# ...
def approve!
  self.approved = true
  self.save
end

Video Deletion

A video can be deleted using the Ziggeo dashboard. When it happens, we also want to remove this video from our database. The event type we are interested in is called video_delete. Once again, find the proper video and then just destroy it:

controllers/api/video_callbacks_controller.rb

# ...
def create
  type = params['event_type']
  respond_to do |format|
    @result = if type == 'video_ready'
                Video.from_api(params['data']['video'])
              else
                if type == 'video_approve' || type == 'video_delete'
                  video = Video.find_by(uid: params['data']['video']['token'])
                  if video
                    type == 'video_approve' ?
                        video.approve! :
                        video.destroy
                  end
                else
                  true
                end
              end
    format.html { @result ? head(:no_content) : head(500) }
  end
end

Nice! Now that we have these callbacks in place, the index action inside the VideosController can be re-written.

Displaying Videos and Meta Information

We don’t need Ziggeo client anymore inside the index action. Instead, simply grab the current user’s videos – only the ones that have been approved:

videos_controller.rb

# ...
def index
  @videos = current_user.videos.where(approved: true)
end

As long as each video now has additional meta information, we can render it on the main page as well:

views/videos/_video.html.erb

<div class="card">
  <div class="card-block">
    <ziggeo ziggeo-video='<%= video.uid %>'
            ziggeo-width="320"
            ziggeo-height="240" ziggeo-popup>
    </ziggeo>
    <p>
      <strong>Duration:</strong> <%= video.duration %>s<br>
      <strong>Created:</strong> <%= video.ziggeo_created_at  %>
    </p>
  </div>
</div>

Deleting Videos via API

The last piece of functionality we will code today is the ability to delete a video from our application. Only the user who owns the video will be able to perform this action.

First, present a “delete” link:

views/videos/_video.html.erb

<ziggeo ziggeo-video='<%= video.uid %>'
        ziggeo-width="320"
        ziggeo-height="240" ziggeo-popup>
</ziggeo>
<!-- ... -->
<p><%= link_to 'Delete', video_path(video.uid), method: :delete %></p>

Note that I am passing a video’s uid, not id – we won’t actually remove videos from the database inside the destroy action.

In order to remove a video, a Ziggeo API client is needed. The deletion is performed using the delete method that accepts a uid.

videos_controller.rb

# ...
def destroy
  video = current_user.videos.find_by(uid: params[:id])
  if video
    ziggeo = Ziggeo.new(ENV['ZIGGEO_KEY'], ENV['ZIGGEO_SECRET'], ENV['ZIGGEO_ENCRYPTION'])
    ziggeo.videos.delete(video.uid)
    flash[:success] = 'Video removed! It may take some time to reflect changes on the website.'
  else
    flash[:warning] = 'Cannot find such video...'
  end
  redirect_to root_path
end

We only remove a video from Ziggeo. After this operation is completed, a video_delete event will be sent to our callback and the corresponding record will be deleted from the database. This process is not instant, that’s why we are warning the user that it may take some time to reflect the change.

Conclusion

We’ve reached the end of this article! Ziggeo presents much more functionality then we went through today as we only discussed its basic features. Therefore, be sure to read more about this service and try it out for yourself. Also, note that Ziggeo is available as a Heroku add-on and can be integrated with such popular services as YouTube and Dropbox.

I hope you’ve enjoyed reading this article and I thank you for staying with me. Happy coding and see you!

  • Joshua Goldstein

    Nicely written article! Just wanted to clarify that it is indeed possible to filter the index call by approved videos:

    @videos = ziggeo.videos.index(“approved” => “APPROVED”)

    Cheers,
    – Josh

    • Ilya Bodrov

      That’s interesting. Not sure why the support told me quite the opposite but maybe that was a misunderstanding. Cheers!

Recommended
Sponsors
Get the latest in Ruby, once a week, for free.