Video Uploads with Rails and 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 tokenuser_id
(integer, indexed) – foreign key to establish the relation between a user and a videoduration
(decimal) – the video’s duration in secondsziggeo_created_at
(datetime) – date and time when the video was created on Ziggeoapproved
(boolean, indexed) – whether a video was approved by moderator, default isfalse
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!