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.
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).
In the next screen, choose to create a Single View Application for tvOS.
Set the product name to StockFootageTV
and the language to Swift
.
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:
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.
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:
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.
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.
Get the project files
You can find the project files on GitHub.