At Kisko Labs we were lucky enough to receive an Apple TV developer kit from Apple (two actually, because I got one for my personal Apple developer account too). Motivated by the free hardware, I took some time to learn how to develop for the new Apple TV (one TVML app and one “native” app). In this post I’ll walkthrough creating a TVML app with Middleman.

Apple TV (4th generation)

What is TVML/TVMLKit?

In Apple’s words:

The TVMLKit framework enables you to incorporate JavaScript and TVML files in your binary apps to create client-server apps.

In other words your Apple TV app can download JavaScript and XML files from your server (or any HTTP server, we used S3 and CloudFront for our app). The Apple TV will automatically generate the UI for you based on the XML templates your provide.

The apps you can create with TVMLKit are limited, but it looks perfect for media heavy applications (e.g. viewing videos or photos).

If you’ve used an older 3rd generation Apple TV, the applications created with TVML will look very familiar…

What do I need (to know) for this tutorial?

Passing knowledge about creating sites with Middleman will suffice (I’m going to assume that you have a recent version installed). You should also have Xcode 7.1 installed and be vaguely familiar with using it. An actual Apple TV is not required (the simulator will suffice).

The versions I used:

  • Xcode 7.1
  • Middleman 3.4
  • Ruby 2.2

I also assume that you know how to navigate around and run commands in a terminal environment.

What will this tutorial cover?

Creating a new TVML app in Xcode, creating a simple backend for it with Middleman, and using the app to play videos from the Internet Archive’s 35mm Stock Footage collection.

0. Initial setup

Make sure that you have Xcode 7.1 and Middleman 3.4 installed. Then create a directory for the work in this tutorial:

mkdir StockFootageTV
cd StockFootageTV

1. Create the Middleman app

Start by creating a new Middleman app called backend in the directory you just created:

middleman init backend
cd backend

We’ll be using Slim to generate the XML for us, so add the following line to the Gemfile:

gem "slim", "~> 3.0.6"

Once this is done, run bundle install.

Then create a new file called application.js.coffee in the source/javascripts/ directory with the following contents:

createAlert = (title, description) ->
  alertString = """<?xml version="1.0" encoding="UTF-8" ?>
    <document>
      <alertTemplate>
        <title>#{title}</title>
        <description>#{description}</description>
      </alertTemplate>
    </document>
  """
  parser = new DOMParser
  alertDoc = parser.parseFromString(alertString, "application/xml")
  alertDoc

App.onLaunch = (options) ->
  doc = createAlert("Hello World", "This is a TVMLKit app")
  navigationDocument.presentModal(doc);

We’ll use this as the basis for our TVMLKit app. Next let’s set-up the project in Xcode. You can delete the existing all.js file if you wish, we won’t need it.

2. Create the tvOS app

Open Xcode and select Create a new Xcode project from the start screen (or File → New → Project from the menu).

Create a new Xcode project

In the next screen, choose to create a Single View Application for tvOS.

Choose a template for your new application

Set the product name to StockFootageTV and the language to Swift.

Choose options for your new project

Finally, save the project to the StockFootageTV directory you created in Step 0.

At the end of this step, you should have a directory structure that looks something like this (with a lot more files):

StockFootageTV
├── StockFootageTV
│   ├── StockFootageTV
│   ├── StockFootageTV.xcodeproj
│   ├── StockFootageTVTests
│   └── StockFootageTVUITests
└── backend
    ├── Gemfile
    ├── Gemfile.lock
    ├── config.rb
    └── source

You can test that the Xcode project is working by hitting ⌘ + R in Xcode (you should get an Apple TV simulator with a blank screen).

3. Do some tedious set-up work in Xcode

Before we can crack on with our awesome TVML app, we need to do some chores in Xcode.

First of all, delete the ViewController.swift and Main.storyboard files from the project (select Move to Trash when Xcode asks).

Then select the project in the left sidebar, choose the General tab, and clear the Main Interface box:

Main Interface

Finally, open the Info.plist file from the left sidebar, right click and click Add Row and paste in NSAppTransportSecurity. It should automatically expand to “App Transport Security Settings” and the Type should be set to Dictionary.

Click on the arrow to expand the entry and add a entry with the key NSAllowsArbitraryLoads (or “Allow Arbitrary Loads” in English). Set the value to YES.

Info.plist edits

4. Set up the tvOS app to load application.js

First of all, start the Middleman development server:

bundle exec middleman server

Go to http://localhost:4567/javascripts/application.js to check that the JavaScript actually loads correctly.

Then open AppDelegate.swift in Xcode and replace its contents with the following (you can leave the comments at the top of the file as they are):

import UIKit
import TVMLKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, TVApplicationControllerDelegate {

    var window: UIWindow?
    var appController: TVApplicationController?
    let baseURL = "http://localhost:4567"

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        window = UIWindow(frame: UIScreen.mainScreen().bounds)

        let context = TVApplicationControllerContext()
        let javaScriptURL = NSURL(string: "\(baseURL)/javascripts/application.js")!

        context.javaScriptApplicationURL = javaScriptURL
        context.launchOptions["BASEURL"] = baseURL

        appController = TVApplicationController(context: context, window: window, delegate: self)
        return true
    }

}

Save the file and run the app again with ⌘ + R. You should be greeted with the following view in the Apple TV simulator:

Hello World!

Good Job! You now have a working, albeit useless, tvOS app!

5. Generate some XML

Since we don’t want to write out all the entries by hand, let’s create a YAML file with a list of our videos. In your Middleman app, create a new file called data/content.yml directory (create the data directory too, if you don’t have it already).

---
video_sections:
  - title: "Various videos"
    videos:
      - title: "Downtown Los Angeles streets"
        description: "Downtown Los Angeles streets, process plates, color."
        url: "https://archive.org/download/PET0981_R-2_LA_color/PET0981_R-2_LA_color_720p.mp4"
        preview: "https://archive.org/download/PET0981_R-2_LA_color/PET0981_R-2_LA_color.thumbs/PET0981_R-2_LA_color_000027.jpg"
      - title: "Hot Rods, Southern California, 1940s"
        description: "Scenes at Southern California racetrack with hot rods and drivers."
        url: "https://archive.org/download/27EG-28-EG-58_HOTRODS/27EG-28-EG-58_HOTRODS_720p.mp4"
        preview: "https://archive.org/download/27EG-28-EG-58_HOTRODS/27EG-28-EG-58_HOTRODS.thumbs/27EG-28-EG-58_HOTRODS_000027.jpg"
      - title: "Internet Archive 35mm Stock Footage Sample Reel"
        description: "This reel shows samples from Internet Archive's 35mm Stock Footage collection."
        url: "https://archive.org/download/InternetArchive35mmStockFootageSampleReel/IA35mmSampleReel_720p.mp4"
        preview: "https://archive.org/download/InternetArchive35mmStockFootageSampleReel/InternetArchive35mmStockFootageSampleReel.thumbs/IA35mmSampleReel_000054.jpg"
  - title: "Comedy"
    videos:
      - title: "The Mechanic (Part 1)"
        description: "Silent comedy taking place in an automobile garage."
        url: "https://archive.org/download/AFP-19-OJ_R-2_TheMechanic_A/AFP-19-OJ_R-2_TheMechanic_A_720p.mp4"
        preview: "https://archive.org/download/AFP-19-OJ_R-2_TheMechanic_A/AFP-19-OJ_R-2_TheMechanic_A.thumbs/AFP-19-OJ_R-2_TheMechanic_A_000054.jpg"
      - title: "The Mechanic (Part 2)"
        description: "Silent comedy taking place in an automobile garage. 2/2"
        url: "https://archive.org/download/AFP-19-OJ_R-2_TheMechanic_B/AFP-19-OJ_R-2_TheMechanic_B_720p.mp4"
        preview: "https://archive.org/download/AFP-19-OJ_R-2_TheMechanic_B/AFP-19-OJ_R-2_TheMechanic_B.thumbs/AFP-19-OJ_R-2_TheMechanic_B_000005.jpg"

Then create a new file called videos.xml.slim in the source directory. Add the following contents to it:

doctype xml
document
  catalogTemplate
    banner
      title 35mm Stock Footage
    list
      - data.content.video_sections.each do |video_section|
        section
          listItemLockup
            title= video_section.title
            decorationLabel= video_section.videos.size
            relatedContent
              grid
                section
                  - video_section.videos.each do |video|
                    lockup videoURL=video.url
                      img src=video.preview width=500 height=308
                      title= video.title
                      description = video.description

Then open up the config.rb file, and configure Middleman not to use layouts for XML files by adding the page "*.xml", layout: false line to the Page options, layouts, aliases and proxies section of the file.

# ... snip ...

###
# Page options, layouts, aliases and proxies
###

page "*.xml", layout: false

# ... snip ...

Finally, open http://localhost:4567/videos.xml in your browser to ensure that the XML is being generated correctly.

videos.xml in Safari

If you’ve done everything correctly, the backend directory should have the following contents:

backend
├── Gemfile
├── Gemfile.lock
├── config.rb
├── data
│   └── content.yml
└── source
    ├── images
    │   ├── background.png
    │   └── middleman.png
    ├── index.html.erb
    ├── javascripts
    │   └── application.js.coffee
    ├── layouts
    │   └── layout.erb
    ├── stylesheets
    │   ├── all.css
    │   └── normalize.css
    └── videos.xml.slim

6. Load the XML from our JavaScript

Open the application.js.coffee file again and replace it with the following:

createAlert = (title, description) ->
  alertString = """<?xml version="1.0" encoding="UTF-8" ?>
    <document>
      <alertTemplate>
        <title>#{title}</title>
        <description>#{description}</description>
      </alertTemplate>
    </document>
  """
  parser = new DOMParser
  alertDoc = parser.parseFromString(alertString, "application/xml")
  alertDoc

readBody = (xhr) ->
  data = undefined
  if !xhr.responseType or xhr.responseType == 'text'
    data = xhr.responseText
  else if xhr.responseType == 'document'
    data = xhr.responseXML
  else
    data = xhr.response
  data

App.onLaunch = (options) ->
  xhr = new XMLHttpRequest
  xhr.onreadystatechange = ->
    if xhr.readyState == 4
      xml = readBody(xhr)
      console.log xml

      parser = new DOMParser()
      doc = parser.parseFromString xml, "application/xml"

      doc.addEventListener "select", (event) ->
        el = event.target
        videoURL = el.getAttribute("videoURL")

        if videoURL != null && videoURL != ""
          player = new Player()
          playlist = new Playlist()
          mediaItem = new MediaItem("video", videoURL)

          player.playlist = playlist
          player.playlist.push(mediaItem)
          player.present()

      navigationDocument.pushDocument(doc)

    return
  xhr.onerror = ->
    errorDoc = createAlert("Evaluate Scripts Error", "Error attempting to evaluate external JavaScript files.")
    navigationDocument.presentModal(errorDoc);

  xhr.open "GET", "#{options.BASEURL}/videos.xml", true
  xhr.send null

This is adds a readBody helper to make handling XHR responses a bit easier and sets up the application to load our videos.xml file. Note that for some reason tvOS applications require a fully qualified URL when loading resources (you can’t just read /videos.xml).

7. Try it out

Now if everything’s gone right, you can go back to Xcode and run the app in the simulator again with ⌘ + R. You should see a more useful tvOS app! To navigate around, enable the Apple TV Remote from the Edit menu.

The final app in the simulator

Get the project files

You can find the project files on GitHub: