Tutorial: How to build a video record feature on your Rails app with Cloudinary

Yann Klein
Le Wagon Tokyo
Published in
8 min readAug 27, 2020
Photo by Mateus Campos Felipe on Unsplash

When it comes to uploading videos on Youtube, TikTok or other social media from a smartphone, it is very natural to press the recording button, shoot and upload the video on the spot.

Surprisingly enough, it is not a standard feature of computer browsers as of today. It is pretty easy to create a video file upload feature on a website, but a recording + upload feature will need a bit more effort. This tutorial will help you build the latter.

The setup

For this tutorial, I will build a website with Rails. I will use the excellent Le Wagon Rails template as a base:

rails new \
--database postgresql \
-m https://raw.githubusercontent.com/lewagon/rails-templates/master/minimal.rb \
my_video_recording_app

This base template will build my website backbone. I specify that I want to use PostgreSQL as a database, and I name my app “my_video_recording_app” (how original!).

I am planning to use Cloudinary, to store my videos. So, I will add the following gems to my Gemfile:

# my_video_recording_app/Gemfilegem ‘dotenv-rails’, groups: [:development, :test]
gem ‘cloudinary’, ‘~> 1.16.0’

I will bundle the gems and install ActiveStorage, the modern way to attach files to Models on Rails:

bundle install
rails active_storage:install
rails db:migrate

Last but not least, I will have to grab my Cloudinary key from my Cloudinary account, create a .env file in my app folder and save it there:

# my_video_recording_app/.envCLOUDINARY_URL=cloudinary://8723123456764639:vJyTWIPF1234567890Oi18@yagthihdky

I will also have to set Cloudinary as my app file storage tool, here:

# my_video_recording_app/config/storage.yml
cloudinary:
service: Cloudinary

And here (note, replace the local one!):

# my_video_recording_app/config/environments/development.rb
config.active_storage.service = :cloudinary

…and here (if you deploy your app in production later on):

# my_video_recording_app/config/environments/production.rb
config.active_storage.service = :cloudinary

Uffff that was a lot of setup! Let’s create my Video model now.

Create a simple video model

My tutorial website will be simple. I want to be able to record video, store them and watch them again.

I will build a Video model first, a Video will have a title, a description and… a video attached! (I call that one file in my model) I don’t want to bother with a full app creation, so I will use the convenient scaffold feature of Rails:

rails g scaffold Video title description
rails db:migrate

And to be able to attach video files to my Video, I will add this line in video.rb:

# my_video_recording_app/app/models/video.rbclass Video < ApplicationRecord
has_one_attached :file
end

Great, now I’ll run my Rails app with rails s and see if I can publish new videos.

localhost: 3000/videos is sadly empty, waiting for me to create some videos.

When I jump, on localhost: 3000/videos/new I can see that my video creation form lacks a “Attach a file” field.

Let’s build that field:

# my_video_recording_app/app/views/videos/_form.html.erb...<div class="form-inputs">
<%= f.input :title %>
<%= f.input :description %>
<%= f.input :file, as: :file %>
</div>
...

And make sure we can upload a video in the controller:

# my_video_recording_app/app/controllers/video_controller.rb...privatedef video_params
params.require(:video).permit(:title, :description, :file)
end
...

Now that feel better:

Time to add a video tag on our index page and then, try to upload our first video file:

# my_video_recording_app/app/views/videos/index.html.erb...<% @videos.each do |video| %>
<tr>
<td><%= video.title %></td>
<td><%= video.description %></td>
<td><video src="<%= url_for(video.file) %>" height="100" width="200" loop muted="muted" autoplay></video></td>
...

Look at the result 😻 but wait… that’s not really why we’re here today. Uploading is fine but I definitely want to record!!

Record a video

For that part, I will need some good JavaScript skills 💪

Build the HTML

First, I will build a “Start” button, a “Stop and save” button and a video area on my video form.

# my_video_recording_app/app/views/videos/_form.html.erb...<h2>Record your video</h2>
<button id="start">Start</button>
<button id="stop">Stop and save</button>
<div>
<video style="border: 1px solid black" id="live" width="300" height="200" autoplay muted></video>
</div>

Display the live camera

I will create a JavaScript file and folder in my project’s javascript/folder to show my laptop camera in the form<video> tag.

# my_video_recording_app/app/javascript/components/record_video.jsconst initRecordVideo = () => {  
const start = document.getElementById("start");
const stop = document.getElementById("stop");
const live = document.getElementById("live");
const stopVideo = () => {
live.srcObject.getTracks().forEach(track => track.stop());
}
stop.addEventListener("click", stopVideo); start.addEventListener("click", () => {
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})
.then(stream => {
live.srcObject = stream;
live.captureStream = live.captureStream || live.mozCaptureStream;
return new Promise(resolve => live.onplaying = resolve);
});
});
}
export { initRecordVideo };

I also need to run my new create function in my application.js :

# my_video_recording_app/app/javascript/packs/application.js...import { initRecordVideo } from '../components/record_video';document.addEventListener('turbolinks:load', () => {
if(document.querySelector("#live")) {
initRecordVideo();
}
});

With that code, I can see my triumphing face 🎉😬👍 on the website by clicking Start and stop the camera with Stop. But what did the code do?

I first listened to a click on my start button and triggered thegetUserMedia JavaScript function. It asks my user if she/he is okay to share the laptop camera and send a Promise with the video stream if the answer is yes ❤️💍

I then get this stream and display it on my <video> tag via live.srcObject = stream.

Record the live stream in a Blob

Let’s add the recording part to our code:

# my_video_recording_app/app/javascript/components/record_video.jsconst initRecordVideo = () => {  const start = document.getElementById("start");
const stop = document.getElementById("stop");
const live = document.getElementById("live");
const stopVideo = () => {
live.srcObject.getTracks().forEach(track => track.stop());
}
// stop.addEventListener("click", stopVideo); const stopRecording = () => {
return new Promise(resolve => stop.addEventListener("click", resolve));
}
const startRecording = (stream) => {
const recorder = new MediaRecorder(stream);
let data = [];
recorder.ondataavailable = event => data.push(event.data);
recorder.start();
const stopped = new Promise((resolve, reject) => {
recorder.onstop = resolve;
recorder.onerror = event => reject(event.name);
});
const recorded = stopRecording().then(
() => {
stopVideo();
recorder.state == "recording" && recorder.stop();
}
);
return Promise.all([
stopped,
recorded
])
.then(() => data);
}
start.addEventListener("click", () => {
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})
.then(stream => {
live.srcObject = stream;
live.captureStream = live.captureStream || live.mozCaptureStream;
return new Promise(resolve => live.onplaying = resolve);
})
.then(() => startRecording(live.captureStream()))
.then (recordedChunks => {
const recordedBlob = new Blob(recordedChunks, { type: "video/webm" });
console.log(recordedBlob);
})
});
}export { initRecordVideo };

I hear you already:

“Yann WTF?? It was Care Bear JavaScript 🐻 until now and boom 💥 things get berserk!!!”

Worry no more my friend, I’ll explain the outline of that code step-by-step (to go even further, check out that excellent MDN doc where I robbed this piece of code):

When we click Start, after we show the live camera, we call the startRecording function. This function creates a MediaRecorder which is basically a JS object that stores a video stream. The function return 2 Promises, meaning that it will be completed and ready to go to the next .then() when these 2 are completed. The first promise is complete when we press Stop (the const recorded one) and the second when the recorder stops (which happens in the first Promise with recorder.stop() ).

Long story short, it starts recording when we click Start and stops when we click Stop (simple enough 😜?).

After I click Stop, that last part happens:

# my_video_recording_app/app/javascript/components/record_video.js....then (recordedChunks => {
const recordedBlob = new Blob(recordedChunks, { type: "video/webm" });
console.log(recordedBlob);
})
...

It takes the recorded video stream and turn it into a Blob (= file) that I console.log and tadaaaa 🎊

On my Chrome console, I can see a Blob (= file)!! Full of data!! Look at the size. I created a WebM file and not a Mp4 file as it’s an open web standard, but video/mp4 works all the same.

Upload the file to Cloudinary

So, at that step you would tell me:

“Oh Yann, I got it, so easy, you just will pass the blob to the form video input and done, sayonara blob-san, say hello to Cloudinary!

My dear reader, you’re too quick. It doesn’t seem possible to directly append a blob to a HTML <form>. Yes, I was as sorry as you are now.

To upload the video to Heroku we will need to 1) Artificially create the data of a form, it is called a FormData 2) append it our video 3) send all this mess via a Rails AJAX request..! Let’s do it 🤘that’s the last step of this tutorial.

I first add import Rails on the top of my JS file to have access to Rails.ajax the rails built-in function to make AJAX requests.

# my_video_recording_app/app/javascript/components/record_video.jsimport Rails from "@rails/ujs";...

Then, I add a Cloudinary Upload function create a FormData from my original form and appending the recorded video in my initRecordVideo function:

# my_video_recording_app/app/javascript/components/record_video.jsimport Rails from "@rails/ujs";const initRecordVideo = () => {  const form = document.querySelector("form");  const uploadToCloudinary  = (video) => {
const formData = new FormData(form);
formData.append('video[file]', video, 'my_video.mp4');
Rails.ajax({
url: "/videos",
type: "post",
data: formData
})
}
...

Finally,. I call this function in my .then() :

# my_video_recording_app/app/javascript/components/record_video.js....then (recordedChunks => {
const recordedBlob = new Blob(recordedChunks, { type: "video/mp4" });
console.log(recordedBlob);
uploadToCloudinary(recordedBlob);
})
...

Okay, all good, I just need now to create a new video, record my self and…

Success 🎊 🎉 Ufff! what an adventure, we finally have our video recording feature!

I hope you enjoyed this tutorial here is the complete source code on Github and the live website to help you.

Find more info about me here and Le Wagon Tokyo coding bootcamp(we have a lot of other tutorials and workshops besides our world #1 coding learning program 💪).

--

--