Apple TV Hacking: Spelunking into the AirPlay video service
When Apple announced its new iOS 4.2 feature set, Apple TV's AirPlay video service really caught our attention. That's why we were particularly disappointed when 4.2 debuted on Monday -- only Apple's own applications could ship video from iDevices to the 2nd generation Apple TV; third party applications were limited to audio transport only.
We contacted Apple PR to ask for a statement about that, asking why the feature shipped without third party support -- and whether it had to do with performance licenses or similar legal matters. TUAW has not yet heard back from Apple at the time this post went live.
Mike Rose asked if I could poke into the situation and see what's going on under the hood. Here's what I found.
Big Massive Update: Thanks to Steven Troughton-Smith, the code has been reduced to a single line with no YouTube work-arounds needed. Details appended to the bottom of the post...
AirPlay Video is part of a public framework called MediaPlayer. This is the same MediaPlayer framework that developers use to show video in their applications. The current movie players ship with an AirVideo selection option built right in. The problem is that when you select AirVideo in a non-Apple application, the video continues to play on-device; only the audio is re-routed through the server to Apple TV.
That's a big bummer, especially when applications like AirVideo and VLC are crying out for this kind of functionality.
So I went to work. Using Kenny TM's amazing class-dump-z, I decompiled headers for the MediaPlayer framework. This produces a complete API list regardless of whether classes and method calls have been formally published by Apple or not. In that framework, I discovered two key classes: MPAirPlayVideoServiceBrowser and MPAirPlayVideoService.
These classes correspond quite closely to Apple's Bonjour standards and it took very little guessing to realize that the service browser used Zero Configuration Networking discovery to find service clients on the local network and that the service class provided a way to push data to those clients.
I subclassed the basic browser so I could add service discovery features and told the browser to start searching for AirVideo servers. (Yes, Apple uses AirVideo in its internal naming scheme. Expect a lawsuit with the AirVideo people momentarily. Apple will probably win.)
It's that browser subclass that provides the magic.
-(void)_didFindService:(id)service moreComing:(BOOL)coming
{
NSLog(@"Service Found. Attempting to resolve: %@ (%d more to come)", service, coming);
[super _didFindService:service moreComing:coming];
[[service retain] setDelegate:self];
[service resolveWithTimeout:0.0f];
}
Using standard NSNetService calls, I went ahead and resolved the service and created a standard TCP connection to it. I had no problem locating the correct service (it's basically built into the class already) and resolving to the correct device and port.
2010-11-24 09:17:48.052 HelloWorld[80:307] Service Found. Attempting to resolve: <NSNetService 0x150320> local. _airplay._tcp. Apple TV (0 more to come)
2010-11-24 09:17:48.098 HelloWorld[80:307] NSNetService did resolve
2010-11-24 09:17:48.104 HelloWorld[80:307] 192.168.0.100:7000
Then came the hard part, trying to build the server and assign media to it. In the end, I kinda sorta got it working but I found that I needed to resort to kickstarting things in the YouTube application before everything got working my application. This is all proof-of-concept pre-alpha development. We take what we can get. Take anything I did with a tablespoon of salt. I wasn't exactly working from official docs.
Upon resolving, I created the MPAirPlayVideoService object and thereafter treated it as a standard movie player. I created a player item, here an MPAVItem, initialized it with a path to my movie, added it to the player service and asked it to play.
self.apvs = [[[MPAirPlayVideoService alloc] _initWithNSNetService:service] autorelease];
id item = [[[MPAVItem alloc] initWithPath:CINDY_PATH error:&error] retain];
[apvs setItem:item];
[apvs prepareToDisplayItem:item completionHandler:
^(id whatever, NSError *error){NSLog(@"Prepared");}];
[apvs playReturningError:&error];
Now the bad news. Clearly these APIs are preliminary and a bit unstable. I started working on this last afternoon and encountered several "Now would be a great time to reboot" scenarios that included my entire device going black except for the status bar, where the home button stopped doing...anything.
So caveat hackteur -- this isn't going to be appropriate for the casual developer. Yet.
Having gotten this proof of concept working, there's still a lot left to get done to transform this into a stable solution that works with general applications. Keep in mind that you'll be working with unpublished APIs, so the above classes and code are not App Store Safe. That's why we have a jailbreak world, after all.
In the end, this kind of thing always ends up in Apple's hands. I strongly encourage all of you to hop on over to bugreport.apple.com and put in a feature request to expose this functionality through an open API and to extend it to general UIView mirroring.
Apple's bug reporting website is a popularity contest. Each feature request is a vote; the more votes a feature gets, the more likely Apple is to respond to that request. OpenRadar is an open source public community bug reporting site that helps developers organize to promote bug fixes and feature requests that matter to them.
Steven Troughton-Smith's One Line Fix:
So I'm chatting with Steven about improving the code and wondering if YouTube uses a special improved player class that has the AirPlay video stuff built in. And Steven says: "What if there's just a flag on the [standard] video player." A few minutes later he had the following code up and working -- and it works great.
So have at it folks. Yes, it still won't be App Store safe, but it's jailbreak friendly, works flawlessly, and suggests only a single item that Apple could move to a public API to open up this functionality to developers. What's more, with a little screen scraping or off-screen layer manipulation and a clever use of AVFoundation, you can probably have games working out to Apple TV almost immediately.
self.movieController = [[[MPMoviePlayerController alloc] initWithContentURL:[NSURL fileURLWithPath:CINDY_PATH]] autorelease];
[movieController setAllowsWirelessPlayback:YES];
movieController.view.frame = self.view.bounds;
[self.view addSubview:movieController.view];