Twitter API: Uploading Videos using Node.js

I work for an influencer marketing and data company, Speakr, and we work with social media APIs all the time. This can be... challenging (to put it lightly!).

One particularly difficult task has been posting videos to Twitter. If you search the internet, it seems we're all trying to figure this out and there isn't a clear answer.

In my opinion, the best Twitter package for Node.js is Twit, but uploading videos using Twit isn't explained well in the docs. Good news for you - I've gone through the pain, and frustration so you don't have to.

Here's how I upload videos to Twitter using Node.js (version 6 or greater) and Twit (version 2.2.5).

Below, I provide two solutions (both use Twit):

  1. Uploading to Twitter when you have the video saved to disk already.
  2. Uploading a video when you need to first retrieve the video from remote storage, like an AWS S3 bucket.

Video Format Warning

Make sure the video is the correct format. Smartphone videos are often a safe bet, but they don't always work. The requirements are outlined in the infamously unhelpful Twitter API docs: https://dev.twitter.com/rest/media/uploading-media

Formatting a Video for Testing Purposes

Here's a great trick I learned from my web searches:

  • Open your video in QuickTime.
  • From the File menu, select Export.
  • Choose the iPad, iPhone, iPod Touch... export option.
  • Choose any iPhone format.

Once it's converted, change the file type to .mp4 by renaming the file.

Twitter Video Upload: Three Steps

Uploading a video to Twitter involves three steps:

ONE: POST (upload) the video (line 33 below).

Then, using the data returned from #1...

TWO: POST the returned meta-data (line 38 below).

THREE: POST the status, i.e. the actual tweet (line 44 below).

I haven't included my error handling, because it is specific to the app I'm working on. Be sure to include error handling for all three steps (also for retrieving and reading the file, if you're fetching it from remote storage).

const TWITTER_API_CONSUMER_KEY = require('./path/to/env/TWITTER_API_CONSUMER_KEY');  
const TWITTER_API_CONSUMER_SECRET = require('./path/to/env/TWITTER_API_CONSUMER_KEY');  
const fs = require('fs'); // part of Node, no npm install needed  
const path = require('path'); // part of Node, no npm install needed

const Twit = require('Twit'); // npm install twit

// you will need an object with your Twitter data
const myTweetObj = {  
  content: 'This is the text of the tweet',
  twitter_handle: 'user twitter handle here',
  access_token: 'twitter user token here',
  access_secret: 'twitter user secret here'
};

// This function is called from within a promise,
// so I pass it 'resolve' and 'reject' so my intentions
// are clear to you, the reader. This isn't 
// necessary though, as you can access the 'resolve' 
// and 'reject' living in the outer scope w/o passing
// them to the function as arguments
function _twitterVideoPub(myTweetObj, resolve, reject) {

  const T = new Twit({
    consumer_key:         TWITTER_API_CONSUMER_KEY,
    consumer_secret:      TWITTER_API_CONSUMER_SECRET,
    access_token:         myTweetObj.access_token,
    access_token_secret:  myTweetObj.access_secret
  });

  const PATH = path.join(__dirname, `path/to/video`);

  T.postMediaChunked({ file_path: PATH }, function (err, data, response) {

    const mediaIdStr = data.media_id_string;
    const meta_params = { media_id: mediaIdStr };

    T.post('media/metadata/create', meta_params, function (err, data, response) {

      if (!err) {

        const params = { status: myTweetObj.content, media_ids: [mediaIdStr] };

        T.post('statuses/update', params, function (err, tweet, response) {

          console.log(tweet);
          const base = 'https://twitter.com/';
          const handle = myTweetObj.twitter_handle;
          const tweet_id = tweet.id_str;
          resolve({
            live_link: `${base}${handle}/status/${tweet_id}`
          });

        }); // end '/statuses/update'

      } // end if(!err)

    }); // end '/media/metadata/create'

  }); // end T.postMedisChunked
}

If you need to download the video from remote storage like AWS S3, here's the code I use. It's identical to the above version, with the addition of:

  • request.get() (line 45 below)
  • fs.writeFile() (line 49 below)
  • fs.unlinkSync() (line 68 below)
const TWITTER_API_CONSUMER_KEY = require('./path/to/env/TWITTER_API_CONSUMER_KEY');  
const TWITTER_API_CONSUMER_SECRET = require('./path/to/env/TWITTER_API_CONSUMER_KEY');  
const fs = require('fs'); // part of Node, no npm install needed  
const path = require('path'); // part of Node, no npm install needed

// below the  { encoding: null } indicates
// binary encoding - very unintuitive!
const request = require('request').defaults({ encoding: null }); // npm install request  
const Twit = require('Twit'); // npm install twit

// you will need an object with your Twitter data
const myTweetObj = {  
  video_link: 'https://video-in-aws-or-whatever.mp4',
  content: 'This is the text of the tweet',
  twitter_handle: 'user twitter handle here',
  access_token: 'twitter user token here',
  access_secret: 'twitter user secret here'
};

// This function is called from within a promise,
// so I pass it 'resolve' and 'reject' so my intentions
// are clear to you, the reader. This isn't 
// necessary though, as you can access the 'resolve' 
// and 'reject' living in the outer scope w/o passing
// them to the function as arguments
function _twitterVideoPub(myTweetObj, resolve, reject) {

  const T = new Twit({
    consumer_key:         TWITTER_API_CONSUMER_KEY,
    consumer_secret:      TWITTER_API_CONSUMER_SECRET,
    access_token:         myTweetObj.access_token,
    access_token_secret:  myTweetObj.access_secret
  });

  // temporary file name, the file will
  // be deleted when upload is complete
  const localname = `tempVideo-${Date.now()}`;
  const PATH = path.join(
    __dirname,
    `path/to/temporary/storage/folder/${localname}`
  );
  const mediaUrl = myTweetObj.video_link;

  // get the video from remote storage
  request.get(mediaUrl, function (error, response, body) {

    // save video to disk temporarily, we
    // delete the video after uploading it
    fs.writeFile(PATH, body, function(error) {

      // step ONE
      T.postMediaChunked({ file_path: PATH }, function (err, data, response) {

        const mediaIdStr = data.media_id_string;
        const meta_params = { media_id: mediaIdStr };

        //  step TWO
        T.post('media/metadata/create', meta_params, function (err, data, response) {

          if (!err) {

            const params = { status: myTweetObj.content, media_ids: [mediaIdStr] };

            // step THREE
            T.post('statuses/update', params, function (err, tweet, response) {

              console.log(tweet);
              fs.unlinkSync(PATH); // Deletes media from /tmp folder
              const base = 'https://twitter.com/';
              const handle = myTweetObj.twitter_handle;
              const tweet_id = tweet.id_str;
              resolve({
                live_link: `${base}${handle}/status/${tweet_id}`
              });

            }); // end '/statuses/update'

          } // end if(!err)

        }); // end '/media/metadata/create'

      }); // end T.postMedisChunked

    }); //end fs.writeFile

  }); // end request.get
}

Good luck!

 

If you'd like to be notified when I publish new content, you can sign up for my mailing list in the navbar.