Moovy Channel - Apple TV and JavaScript

By Josh Wright on January 2017 in projects

 

The Problem

I have about 200 DVDs and about 40 seasons of TV shows. And I never watch any of them.

Instead, I turn on the TV and nothing is on. Or there's a movie on, but it's about 40% commercials. And it's not a good movie. Or a rerun is on, but the network only bought 15 episodes, so I see the same ones over and over.

So I thought, what if there was a TV channel that just played my movies and TV shows?

I could flip to "my" channel and one of my movies would always be playing - with no commercials!

For years, I thought this would be great, but there wasn't a good clean way to do it. Then Apple TV allowed third-party apps and everything changed.

The Moovy Channel App

First, a quick walk-through of how the app works. First off, you have to create a few icons and banner images so that things look nice on Apple TV's home screen. Here is Apple's Guide for Icons & Images.

Once you open the app, it shows me the different categories like Movies, Friends, Seinfeld, etc. I'll probably add a Kids category pretty soon. Each of these categories has its own "channel" which is always playing.

Below is a screenshot of the "Movies" category.

"Watch Live" is the live channel that is always playing. I like the concept of starting a movie in the middle because that's how real TV works. I've found that starting from the beginning makes it feel like I have to decide whether I really want to watch this movie. If it starts in the middle, I'm immediately drawn in and I don't second guess as much. I'm not actually playing a movie all of the time, but it feels like that is the case. If you "watch live" and it's 1 hour in to "Die Hard" then in an hour it will be 2 hours in to Die Hard.

"Random" is a lot like the live function, but it just picks a brand new movie. This is handy when you're with some friends and just want to watch something from the start. Don't like what it picked? Try Random again and it will pick something else.

"Latest >" is a new addition. It lists the movies in order from newest to oldest - making it easy to find new releases or movies you haven't watched yet.

The Code

Apple TV apps can be built in one of two ways. You can either use Objective-C or Swift to build views, controllers, etc (much like a normal iPhone). Or, Apple allows you to use JavaScript and templates to render screens, handle click events, etc.

At first, I opted for Swift since I hate working in abstractions. I needed to be able to start a movie half way through and wasn't sure the dumbed-down JavaScript version would be capable. Eventually, I realized that the layout and button maneuvering would be fairly difficult in native code, so I gave JavaScript a shot. JavaScript was extremely easy to implement and I was able to do everything I needed. You're a little more limited on what layouts are possible, so I couldn't implement a full-blown custom interface. But the interface didn't really matter to me as long as it was easy to use.

So AppDelegate.m is actually my only code file in the XCode project. And there's basically one line of code that bootstraps everything by giving the app a url to my JavaScript file. For example:

appControllerContext.javaScriptApplicationURL = [NSURL URLWithString:@"http://example.com/apple-tv-application.js"];

I could have written all of the code in one big JavaScript file, but that would be difficult to organize/maintain. So I wrote it in CommonJS style and used Browserify to bundle everything into one JavaScript file once I was finished developing. I also referred heavily to Apple TV Markup Language Reference (TVML) and Apple TVMLKit JS to know what templates and JavaScript sdks were available. Those offered some direction, but much of the SDK is evolving and undocumented so it took a good amount of trial/error and googling to get things right.

I'd be willing to open source the whole app if someone was interested, but I'll just show some code examples here.

This is an example of showing a new screen within the app. In this case, I'm listing the movies in order of release date. Clicking on the movie will play it.

var _ = require("lodash");
var latestTemplate = require("./latest.ejs");
var playMovie = require("./play-movie");

module.exports = function (categoryId) {
  var category = _.find(catalog.categories, {_id:categoryId});

  var tvml = latestTemplate({_:_, model:{
    category:category,
    movies: _.chain(catalog.movies)
      .filter(function(movie){
        return movie.category_ids.indexOf(categoryId) > -1;
      })
      .sortBy(function(movie){
        return movie.created * -1;
      })
      .value(),
  }});
  var doc = new DOMParser().parseFromString(tvml, "application/xml");

  doc.addEventListener("select", function(e){
    var movieId = e.target.getAttribute("movie-id");
    var movie = _.find(catalog.movies, {_id:movieId});
    var durationInMinutes = Math.round(movie.seconds / 60);
    var durationHours = Math.floor(durationInMinutes / 60);
    var durationMinutes = durationInMinutes % 60;
    var durationString = durationHours+":"+(durationMinutes < 10 ? ("0"+durationMinutes):durationMinutes);
    playMovie(movie, { title: movie.name, subtitle:durationString });
  });

  navigationDocument.pushDocument(doc);
};

Another interesting bit of code is the part that determines what is currently playing on the live channel. It was important to me to make sure that it was consistent - meaning you could come back in 5 minutes and the same movie would still be playing (just 5 minutes later), so I used a random number generator that was seeded by movies created before today.

var randomSeed = require("random-seed");
var _ = require("lodash");

module.exports = function (categoryId) {
  // Get start of today
  var startOfDay = new Date();
  startOfDay.setHours(0,0,0,0);
  var startOfDayTime = startOfDay.getTime();

  // Get movies created before today
  var movies = _.filter(catalog.movies, function(movie){
    return movie.created < startOfDayTime && movie.category_ids.indexOf(categoryId) > -1;
  });

  var totalSeconds = movies.reduce(function(total, movie){ return total + movie.seconds; }, 0);
  var now = new Date().getTime();
  var playlistStart = Math.floor(now / (totalSeconds*1000)) * (totalSeconds * 1000);
  var random = randomSeed.create(Math.floor(playlistStart));
  var playlist = shuffle(movies, random);
  var movieStartTime = playlistStart;

  var movieAndTime = undefined;

  _.each(playlist, function(movie){
    if (movieAndTime) return;

    var movieEndTime = (movie.seconds * 1000) + movieStartTime;
    if (movieEndTime <= now) {
      movieStartTime = movieEndTime;
      return;
    }

    movieAndTime = {
      movie: movie,
      time: Math.round((now - movieStartTime)/1000)
    };
  });

  return movieAndTime;
};

function shuffle(a, random) {
  var indexes = [];
  _.times(a.length, function(i){
    indexes.push(i);
  });

  var b = [];
  while (indexes.length > 0){
    var indexIdx = Math.floor(random.random()*indexes.length);
    var i = indexes[indexIdx];
    b.push(a[i]);
    indexes.splice(indexIdx, 1);
  }
  return b;
}

Another huge part was ripping all my movies to video files. I used DVDDecryptor and HandBrakeCLI for the most part.

We actually use this app all of the time, so it worked out pretty nicely. If you have an idea for an Apple TV app and know a little JavaScript, I highly recommend trying it out.