Convert Videos on Your Phone Into Animated GIFs Using Node, Libav & Imagemagick

October 14, 2014
Written by

highfive

One of the great ironies of the last few years has been the explosion in popularity of the animated GIF. Just as browsers were finally starting to embrace cutting-edge technologies like WebRTC, WebSockets and WebGL, a technology first supported by Netscape 2.0 in 1995 completely took over cultural landscape on internet. You can’t read a sports blog, a tweet or even go to a technology conference without running into the humble animated GIF.

Despite the popularity of animated GIFs, the tools for making them aren’t great. Personally, I wanted something simple that I could use to convert cute videos of my kids on my phone into animated GIFs. Now, there are several tools for converting videos to animated GIFs on your computer. Recently some developers from Yahoo released Gifshot, a tool that converts videos into animated GIFs inside of your browser using open web technologies. But I wanted to build a web service, so I needed to rely on software that was either built-in to or could be easily added to a standard Linux server distro. After some trial and error, I landed on the following combination of technologies:

  • Ubuntu 14.04 on a Digital Ocean VPS
  • Libav – to process the videos and retrieve the frames
  • Imagemagick – to stitch together the animated GIF
  • Node.js – to orchestrate and serve the generated animated GIFs
  • Twilio – to send and receive MMS messages

You can try out a hosted version of the app I built by sending a short video (think Vine, 6 seconds or less) to:

United States: (747) 900-4443

Canada: (778) 655-4263

After being notified that your video has been queued, you should receive an animated GIF version of your video. In this blog post we’ll walk through the process of building this app from scratch by spinning up a Linux VPS, installing the necessary software on it, building a simple Node application to orchestrate the conversion of videos to animated GIFs and wiring it into a Twilio number. This is what you will accomplish when you’re done:

twilio_mms

What You Will Need

Here are the two things you’ll need to get started:

  1. Twilio account with an MMS-enabled phone number
  2. Ubuntu Linux server with Node.js installed

For the purposes of this blog post I’m going to use a Digital Ocean droplet to power this service. If you already have an Ubuntu VPS with Node.js installed you can skip ahead to Step 2.

Step 1: Create a Digital Ocean Droplet

Create a Digital Ocean account and get a free $10 credit with the code twilio10. Once you’re logged in click on the “Create” button to spin-up a new droplet (this is what they call a VPS).

DigitalOcean_Control_Panel

Give your droplet a name and select the smallest plan.

DigitalOcean_Control_Panel2

Scroll down and under the “Select Image” section click on “Applications” and then choose the Node/Ubuntu image. This will spare you from having to manually install Node on your new droplet.

DigitalOcean_Control_Panel3

Finally, click on “Create Droplet”. Once your droplet is up and running the root password will be emailed to you.

Step 2: Install libav and imagemagick

Now it’s time to install the binaries that we’re going to use to process video and create animated GIFs. Open up a terminal program on your local machine and SSH into your droplet. If you haven’t already, please create a non-root user and grant that user sudo privileges. Next, install libav and imagemagick:

sudo apt-get update
sudo apt-get install libav-tools
sudo apt-get install imagemagick

Libav is a set of programs for processing video and you’re going to use the program  avconv to pluck frames out of the video you receive from Twilio. Make sure that it’s working on your system as it should by creating an animated GIF from a video.

mkdir tmp
cd tmp
curl -O http://linode.rabasa.com/bouncing.3gp
avconv -i bouncing.3gp -r 8 -vframes 48 -f image2 bouncing-%03d.jpeg

Here you are passing in a video in the 3gp format.  If you’re curious what video formats that avconv supports on your system just run avconv -formats.

Here is an explanation of the flags used in the example above:

  • -i : the input file
  • -r : the number of frames per second to grab
  • -vframes : the maximum number of frames to grab
  • -f : the output format

The last parameter to the command tells avconv what to name the output files. The “%03d” is a special mask that avconv will use to number each of the files starting at 1. If all goes well you should end up with 48 (or less) jpeg files in the  tmp directory.

Like libav,  imagemagick  is a set of programs for manipulating images. Use the  convert program to stitch the frames together into an animated GIF:

convert -delay 12 -loop 0 bouncing*.jpeg bouncing.gif

There are the flags I’m using:

  • -delay : number of 1/100ths of a second to pause between each frame
  • -loop : causes it to loop over and over

You should now have an animated GIF named bouncing.gif in your directory. Take a look at that GIF in your browser using Node.

Step 3: Create the Node App and Serve Static Files

In your home directory create a new application directory and initialize node:

mkdir apps
mkdir apps/gifit
cd apps/gifit
npm init

When you run npm init  the process will ask a series of questions to help set up your Node application and create a package.json  file. Give it any name you like and use the default entry path ( index.js ). Next, add the node-static module to your app:

npm install node-static --save

This module will make it easy for us to serve the generated GIFs as static files. Now edit index.js  and add some code to serve static files:

var http = require('http')
  , static = require('node-static');

// The directory where our animated GIFs will live
var dir = new static.Server('./public');

// Spin up our HTTP server
http.createServer(function(req, res) {
 req.addListener('end', function () {

   dir.serve(req, res);

 }).resume();
}).listen(process.env.PORT || 3000);

console.log('Listening on port ', process.env.PORT || 3000);

In your application’s directory create a subdirectory called  public  and copy the animated GIF that you generated earlier into the that new directory. Now start-up your Node server:

mkdir public
cp ../../tmp/bouncing.gif public
node .

Open up a browser and go to the IP address of your VPS at port 3000 and append the name of the generated GIF:  http://your_ip_or_domain:3000/bouncing.gif

If everything is working you should see this:

2014-10-09 10.56.48
twilio_mms-1

Now that your system can successfully convert videos into animated GIFs, it’s time to add some logic to your Node application to accept video content from Twilio, orchestrate its conversion and return the animated GIF. Add the following modules to your project. I’ll discuss what these modules do as you build out your service.

npm install glob node-uuid request twilio --save

Go ahead and open up index.js  and add the following code to the top:

var http = require('http')
  , static = require('node-static')
  , os = require('os')
  , fs = require('fs')
  , glob = require('glob')
  , util = require('util')
  , exec = require('child_process').exec
  , request = require('request')
  , url = require('url')
  , twilio = require('twilio')
  , uuid = require('node-uuid')
  , child;

The url module is built-in to Node and will give you an easy way to get information about each web request to your Node application. You might ask why I’m not using a framework like Express or Hapi to build this application. The answer is that this application handles only two kinds of requests and doesn’t do anything fancy with the inputs or outputs. I felt like this was a good opportunity to keep things close to the metal.

Modify your HTTP server code to parse the incoming request and invoke the   handleMessage  function when the path is /message .

// Spin up our HTTP server
http.createServer(function(req, res) {
 req.addListener('end', function () {
   // if the requested path is /message, process the incoming MMS content
   if (url.parse(this.url).pathname === '/message') {
     handleMessage(req, res);
   }
   // else serve static files
   else {
     dir.serve(req, res);
   }
 }).resume();
}).listen(process.env.PORT || 3000);

In the  handleMessage function, you need to determine if the media you’ve been sent is indeed a video. If so, send the user a message that their video has been queued and will be processed shortly. If not, send them a message letting them know that they need to attach a proper video file.

var handleMessage = function(req, res) {
 // Parse the request URL
 var hash = url.parse(req.url, true);
 // This is the phone number of the person who sent the video
 var phone = hash.query['From'];
 // This is the URL of the file being sent
 var mediaUrl = hash.query['MediaUrl0'];
 // This is the content type of that file
 var mediaContentType = hash.query['MediaContentType0'];
 // This is the host the machine serving this Node process 
 var host = req.headers['host']; 

 console.log('Processing MMS: ', mediaUrl, mediaContentType);

 res.writeHead(200, {'Content-type': 'text/xml'});
 var twiml = new twilio.TwimlResponse();
 // if media URL looks like a valid video, send ok back to the user
 if (mediaContentType.indexOf('video') >= 0) { 
   twiml.message('Video queued for processing, hang tight!');
   res.end(twiml.toString());
   processVideo(mediaUrl, host, phone);
 }
 else {
   twiml.message('This is not a video format that we recognize. Try again?');
   res.end(twiml.toString());
 }
}

Now that you have a URL to a video file you can create a function called  processVideo to handle the conversion of this file into an animated GIF.

Step 5: Orchestrate using child_process

twilio_mms-2

At a high level there are a few things that  processVideo  needs to do:

  1. Download the video to the local filesystem.
  2. Call  avconv to convert this video file into frames.
  3. Call  convert  to stitch these frames into an animated GIF.
  4. Send a message to the user with a pointer to this hosted GIF.

Since there will be files created during this process (some of them being temporary and subject to later deletion) you can use the node-uuid module to create a practically unique prefix for these files.

The os.tmpdir  function returns the operating system’s temporary directory which should be available for you to create and delete files in. The  request  module helps you fetch this URL and save it to the temporary directory with the filename of the generated UUID. When the download is finished its time to kick off the processing of the video.

var processVideo = function(mediaUrl, host, phone) {
 // create a unique UUID for all of our video/gif processing
 var id = uuid.v1();

 // Save the remote movie file to the /tmp fs
 download = request(mediaUrl).pipe(fs.createWriteStream(
   util.format('%s/%s', os.tmpdir(), id)));

 download.on('finish', function() {
   // Once it's saved, it's time to spin-up a child process to
   // handle decoding the video and building the gif

   var cmd = util.format('avconv -i %s/%s -r 8 -vframes 48 -f image2 %s/%s-%03d.jpeg && convert -delay 12 -loop 0 %s/%s*.jpeg %s/public/%s.gif && convert %s/public/%s.gif -layers optimizeplus %s/public/%s.gif', os.tmpdir(), id, os.tmpdir(), id, os.tmpdir(), id, __dirname, id, __dirname, id, __dirname, id);
   child = exec(cmd, function (error, stdout, stderr) {
       if (error !== null) {
         console.log('exec error: ' + error);
         client.sendMessage({
           to: phone, from: process.env.TWILIO_CALLER_ID, 
           body: 'Very sorry but an error occurred processing your video. Try a different video?'}, 
           function(err, responseData) { 
             if (err) {
               console.log('Error sending text: ' + err);
             }
           });
       }
       else {
         sendGif(host, id, phone);
       }
       cleanUp(id);
   });
 });
};

There are two ways in Node to start a child process: exec  and spawn . The main difference between the two is that exec buffers output from the child process and returns it in its entirety and spawn streams output from the child process as it comes back. Since you aren’t interested in the output (stdout, stderr, etc) of the process you can simply use exec.

Exec takes a string that represents the operating system command that Node will execute. This command will be identical to what you executed manually earlier. The util module helps to format the command string with variables representing the operating system’s temporary directory, the UUID and the current directory of the Node process. Notice that you are chaining 3 processes together using the && shell operator. This has the effect of only executing subsequent commands if the previous command executed without error.

The second argument to exec is a callback. If any of the processes returned an error, send the user an SMS apologizing for the error. If there was no error, you can write a sendGif  function to send an MMS to the user that includes the generated animated GIF.

Step 6: Send Animated Gif Back to User

twilio_mms-3

In order to send an MMS back to the user you must construct a fully qualified URL to the animated GIF. Twilio will fetch the animated GIF from this location in order to construct the MMS that it delivers to the user. This is easy to do using the host information included in the request headers and the UUID you generated.

var sendGif = function(host, id, phone) {
 // an assumption made here is that the protocol is HTTP
 var gifUrl = 'http://' + host + '/' + id + '.gif';
 console.log('Success! Gif URL: ', gifUrl);
 client.sendMessage({
   to: phone, from: process.env.TWILIO_CALLER_ID, 
   body: 'Powered by Twilio MMS',
   mediaUrl: gifUrl}, function(err, responseData) { 
     if (err) {
       console.log('Error sending MMS: ', err.toString());
     }
   });
};

Step 7: Cleaning Up

Whether the exec process succeeded or not, make sure to clean-up all of the temporary generated files. The glob module makes it easy to get all of the files in a directory that match a given mask.

var cleanUp = function(id) {
  console.log('Cleaning up temp files');
  glob(os.tmpdir() + "/" + id + "*", function (err, files) {
    files.forEach(function(file) {
      fs.unlink(file, function (err) {});
    });
  });
};

Step 8: Testing the App

Save the index.js  file. Now it’s time to wire this app to your MMS-capable Twilio number and take this app for a test drive!

First, set the following environment variables:

export TWILIO_ACCOUNT_SID=xxx
export TWILIO_AUTH_TOKEN=yyy
export TWILIO_CALLER_ID=zzz

The TWILIO_CALLER_ID should be set to same number you’ll be using to receive messages on. Now, start the Node process:  node .

Go to your Twilio account dashboard and click on the number you are going to use. Update the Messaging Request URL  to point at your VPS on port 3000 with a path of /message . Make sure to select HTTP GET , your code is expecting GET parameters to be passed. Click “Save”.

Phone_Number__747__900-4GIF___Dashboard___Twilio

The moment of truth. Send a short video (6 seconds or less) to your Twilio number. You should get an immediate response that your request has been queued, followed by a response that includes the newly minted animated GIF.

2014-10-06 13.30.31

Wrapping-up

In this blog post I walked you through using Node.js to convert videos sent to a Twilio phone number into animated GIFs. This included:

  • Spinning up an Ubuntu VPS with Node.js installed on Digital Ocean
  • Installing Libav and Imagemagick binaries
  • Using Node.js to orchestrate these programs and serve static files
  • Integrating Twilio to accept MMS videos and send out animated GIFs

There are two exercises left to the reader that are necessary to make this a more robust service:

  1. Using a job queue (like Bull) to handle the orderly processing of conversion jobs. This is important because the programs doing the conversion (acconv and convert) are CPU-bound and will quickly soak up system memory and resources.
  2. Configuring your server to run Node as a daemon and start the process on boot.

All of this code is hosted up on Github: https://github.com/crabasa/gifit

Hope you enjoyed this tutorial, happy hacking!