07 Jan 2016, 16:45 Continuous Delivery

Automatically Managing an Apple TV Build Radiator

I’ve recently joined IBM’s Bluemix Garage in London. The Garage is all about being lean and innovative. They have some really nice setups, including two-screen two-keyboard pairing stations, and Apple TVs instead of the more traditional data projectors. Whenever any of us want to present, we just mirror our screen to an enormous TV using AirPlay. No more fiddling with projector cables - bliss! When we’re not using the TV for a presentation, we have it showing our build radiator and our telepresence window. We commit tens of times a day, so having the build radiator always-on means the team notice almost instantly if one of our commits causes a build problem:

In my previous roles, I’ve always loved it when we brought fresh blood into my team. New people don’t know all our habits, and they don’t take stuff for granted, so they ask questions. I’ve now found myself on the other side of that relationship; I’ve been the one asking a lot of - mostly dumb - questions. One of those questions was “why isn’t the TV showing our build radiator anymore?” It turns out that every time we used our TV to share a presentation, we had to walk over and manually re-enable the sharing from the build monitor machine.

Well, I’ve never seen a problem I didn’t want to automate. When I heard the word ‘manual’ I had to drop what I was doing (“thud”) and run off and write a script.

Scripting mirroring a display to an Apple TV

There are some excellent resources on how to use AppleScript to drive the AirPlay menu, and it’s pretty easy (the applescript file I ended up with isn’t very long).

tell application "System Events"
    tell process "SystemUIServer"
        click (menu bar item 1 of menu bar 1 whose description contains "Displays")
        set displaymenu to menu 1 of result
        -- Tolerate numbers in brackets after the tv name --
        click ((menu item 1 where its name starts with tvname) of displaymenu)
    end tell
 end tell

One complexity is that the name of our Apple TV box sometimes gets a number in brackets tacked on to the name, so we can’t just hardcode the name. Using a “starts with” selector fixes that problem.

Driving the script automatically

I also set up a launch agent (a .plist file) to drive the display mirroring every five minutes. The exact plist file will depend on where the scripts are extracted, but copying our .plist file to ‘~/Library/LaunchAgents’ and updating the paths is a good start.

Detecting when an Apple TV is already showing something

This solution wasn’t quite good enough, though. If one of us was ~currently~ sharing our screen, we didn’t want the build radiator to grab control back every five minutes. How could we tell if the screen was currently being used? It turns out that this is harder. My initial assumption was that we’d get a ‘Garage TV is being used by someone else’ prompt if an AirPlay share was active, but we didn’t. I suspect it depends on the version of the Apple TV and also the type of share (photos, videos, mirroring … ). Looking at the AirPlay protocols, there doesn’t seem to be any HTTP or RTSP endpoint on the Apple TV which reports whether it’s currently active. That’s ok - we can figure it out ourselves.

AirPlay mirroring traffic is all handled on port 7000 of the Apple TV (other types of share uses different ports). We can sniff the network traffic to see if there are packets flying around, and only start AirPlay mirroring if there aren’t. Bonjour allows us to find an ip address if we know the device name, and tcpdump shows us traffic.

To get the IP address of an Apple TV, replace spaces with hyphens in the name, append a .local domain, and then ping it. The ping output will include the ip address, which can be parsed out. For example,

 # Substitute dashes for spaces to find the Bonjour name
 tv hostname=${tvname/ /-}.local
 ipaddress=$(ping -c 1 $tvhostname | awk -F'[()]' '/PING/{print $2}')

Then tcpdump can be used to monitor traffic to that host. My initial implementation was this:

 sudo tcpdump tcp port 7000 and host $ipaddress > /var/tmp/airplay-tcpdump-output

The tcpdump command will run indefinitely, so we let it run for ten seconds, then stop it:

# Get the PID of the tcpdump command
pid=$!
# Capture 10 seconds of output, then kill the job
sleep 10
sudo kill $pid

You may need to add tcpdump (and kill) to the sudoers file so that it doesn’t prompt for a password:

 buildmachine ALL=(ALL:ALL) NOPASSWD: /usr/sbin/tcpdump
 buildmachine ALL=(ALL:ALL) NOPASSWD: /bin/kill

This worked great when I tested locally, but when I tried it on a bigger network, I discovered that on a wireless network, traffic is point-to-point, and tcpdump can’t gather packets promiscuously. In other words, I could only see traffic to the Apple TV when it was coming from my machine. Useful, but not useful enough.

Switching to monitor mode allows all packets to be viewed, but since packets on a secure network are encrypted, tcpdump can’t filter on tcp-level details, like the port and IP address. Luckily, tcpdump does allow filtering by MAC address, and the arp -n command can be used to work out the MAC address from the IP address.

 arp -n $ipaddress &> /var/tmp/arp-output
 fieldindex='$4'
 macaddress=`awk -F"[ ]" "/($ipaddress)/{print $fieldindex}" /var/tmp/arp-output`

Finally, the output file can be parsed with awk to count how many packets have flown past.

# Process the output file to see how many packets are reported captured
packetcount=`awk -F'[ ]' '/captured/{print $1}' /var/tmp/airplay-tcpdump-output`

A heartbeat packet is sent every second, so I assumed that after listening for 10 seconds an in-use device will always generate some traffic. Listening to the tcp packets on port 7000 (my first attempt) doesn’t yield very many packets, so any packet count greater than 0 indicates the device is in use. Once I started listening to the lower-level packets, there was always some traffic, even when the tv isn’t in use, so I used a threshold of 20:

if [ $packetcount -gt 0 ]
then
    # Handle in-use case
else
    # Handle not-in-use case
fi

The pieces are connected together in the shell script.

Digging in the depths of Apple TV

If you want to do more hacking with AirPlay, the following command lists all devices on a network: dns-sd -B _airplay._tcp

To see the details for an individual device from the list, which includes version and configuration information, you can do (for example)

dns-sd -L "6C94F8CF0F3F@Garage TV" _raop._tcp local

That gives

Lookup 6C94F8CF0F3F@Garage TV._raop._tcp.local
DATE: ---Thu 05 Nov 2015---
16:38:28.679  ...STARTING...
16:38:28.680  6C94F8CF0F3F@Garage\032TV._raop._tcp.local. can be reached at Garage-TV.local.:7000 (interface 4)cn=0,1,2,3 da=true et=0,3,5 ft=0x5A7FFFF7,0x1E md=0,1,2 am=AppleTV3,2 pk=d8b7d729ee2cd56d19e1fe2c9396f63fa41b227727e1b510dc925396d2d86668 sf=0x204 tp=UDP vn=65537 vs=220.68 vv=2