I want to share what I thought was a clever idea from a member of Apple’s DTS team. I’d been working on an app featuring a custom video player and video delivered via HTTP streaming. The challenge centered on how to elegantly decrypt an encrypted video stream in the app without transmitting the key with the stream. Some examples I Googled talked about serving the video up over a custom protocol and using a subclass of NSURLProtocol and then manually decrypting the stream, and while these worked, it seemed like a lot of unnecessary work. As it turned out subclassing NSURLProtocol was the right place to start but the final solution was so much simpler than the examples I’d seen.
A Quick Overview
I’ll direct you to Apple’s documentation on their HTTP Streaming Architecture for a more thorough reading but here’s a quick overview.
HTTP Streaming allows you to stream either live or prerecorded video with support for authentication and encryption. Your encoded video is processed through Apple’s media segmenter. The segmenter splits up the video into small files with the .ts extension and an associated index that is saved as an .m3u8 file. The .m3u8 file is an text document containing playback information about the stream such as the order of the .ts files, different resolutions, authentication and encryption requirements.
To play the video load the URL for the .m3u8 file in a player that supports the format (QuickTime for example, or MPMoviePlaybackController) and the player should handle the rest.
What if I Want a Secured Video Stream?
Apple’s HTTP Streaming Architecture allows you to encrypt the video stream. If you choose this option a key file is created that contains the decryption key for your video. By default, the location of this key file is included in the .m3u8 file, and allows media players to obtain your key and decrypt the stream. Apple also provides support for delivering the key over HTTPS vs HTTP.
The app I was building was a custom player and would be sole playback solution for the streaming video. This provided me with a different option, which was to include the key file in the app rather than specifying it in the .m3u8 file. This was great because it fulfilled the requirement of the app being the only playback outlet for the video, but by removing the key file reference from the m3u8 file, the MPMoviePlayerController could no longer automatically decrypt the video. I’d need to find another way to do that.
Decrypting Streaming Video via NSURLProtocol
The solution was to keep a reference to the key file in the .m3u8 file, but to specify a custom protocol rather than HTTP or HTTPS. Then, using a subclass of NSURLProtocol, intercept any requests made via the custom protocol and instead return the key file included in the app. Thereby providing the key that lets the MPMoviePlayerController use its existing decryption methods to decrypt and play the stream.
The code isn’t that complicated. First, create your subclass of NSURLProtocol. You will be providing your own implementations of the following methods:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request; + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request; - (void)startLoading; - (void)stopLoading
First is canInitWithRequest. In this method you will compare the url scheme of the request with the name of your custom protocol and return true if they are the same. It doesn’t matter what you name your protocol as long as the name is consistent between what is in the .m3u8 file and your subclass of NSURLProtocol.
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
NSString *scheme = [[request URL] scheme];
return ([scheme caseInsensitiveCompare:@"name_of_your_custom_protocol"] == NSOrderedSame);
}For canonicalRequestForRequest it is sufficient to just return the original request.
Next is the meat of the matter: the startLoading method. This is where we load the key file included in the app bundle, and return it as if it were the result of the HTTP request. First, load the key file as an NSData object.
NSString *keyPath = [[NSBundle mainBundle] pathForResource:@"name_of_your_key_file" ofType:@"key"]; NSData *keyData = [NSData dataWithContentsOfFile:keyPath];
Next, create an NSURLResponse object that is a text mime type and has a content length matching the size of the NSData object created in step one.
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[request URL] MIMEType:@"text/plain" expectedContentLength:[keyData length] textEncodingName:nil];
Finally, return the key data by manually calling didReceiveresponse and didLoadData methods. Don’t forget to release the data and response objects afterward.
id client = [self client]; [client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; NSData *data = [[NSData alloc] initWithBytes:[keyData bytes] length:[keyData length]]; [client URLProtocol:self didLoadData:data]; [client URLProtocolDidFinishLoading:self]; [data release]; [response release];
When you put everything together it should look something like this:
- (void)startLoading {
NSURLRequest *request = [self request];
// Grab the key from the file and install it to buff16
NSString *keyPath = [[NSBundle mainBundle] pathForResource:@"name_of_your_key_file" ofType:@"key"];
NSData *keyData = [NSData dataWithContentsOfFile:keyPath];
// Now return the data as a url response.
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[request URL] MIMEType:@"text/plain" expectedContentLength:[keyData length] textEncodingName:nil];
id client = [self client];
[client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
NSData *data = [[NSData alloc] initWithBytes:[keyData bytes] length:[keyData length]];
[client URLProtocol:self didLoadData:data];
[client URLProtocolDidFinishLoading:self];
[data release];
[response release];
}If everything was implemented correctly you should be decrypting your secured streaming video.
Enjoy.

Hey,
Thanks for the info. But we tried using the NSURLProtocol framework overriding with MPMoviePlayerController. But, looks like this doesnt use the custom URL framework and control never hits the startloading function if we modify the keyurl with custom protocol and feed this to MPMoviePlayerController.
Have you tried using MPMoviePlayerController with NSURLProtocol
I’m not sure what it is your asking exactly, but the approach I described worked well for the project I was working on. I’m sure there are other approaches that come with their own unique set of pitfalls as well.
Hey,
That’s great if it worked for you. I am trying exactly similar. But is unable to proceed because of following:
Player used: MPMoviePlayerController
Server was hosting the m3u8 file with following:
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI=”customurl-file://localhost/2500/key0.bin”
#EXTINF:10,
http://localhost/2500/ts_in_0.ts
#EXTINF:10,
http://localhost/2500/ts_in_1.ts
#EXT-X-ENDLIST
- Created the interface deriving from NSURLProtocol that support canInitWithRequest using protocol “customurl-file”.
- Registered the new interface class with the NSURLProtocol class.
Gave the m3u8 file url path http://localhost/1.m3u8 to the MPMovieMediaController.
Expected result:
I was expecting that the MPMoviePlayercontroller to download the m3u8 file and then download the key using the custom protocol.
I was expecting that when the MPMoviePlayercontroller will download the key, then startLoading method of new interface class will hit.
But this didnt happened. startLoading was not hit.
Can you throw some light how you achieved it.
Thanks a lot
If you’re looking for some help with your app a better place to ask would be on stackoverflow.com. If you ask there the whole community can try to help, and their answers will be easier to find for the next person who has a similar question.
Anyway, your example shows that your server is your localhost. A device is never going to be able to download the contents of the m3u8 that way. Aside from that, make sure that you’ve correctly set up your custom protocol. Those are the things I suggest you look at first.
Cheers.
Hey java,
could you please post your sample project, which worked for your?
Hi Cheers,
I don’t have a sample project to share currently (all the work I did was for a client) but I might find it interesting to put one together sometime. If I get the bandwidth to do it I’ll ping you back with a link to the source. Thanks for the suggestion
Hi man, good post!!!
but an Xcode project (with all the classes explained) will be very useful.
Do you have some examples to post?
Thanks nick.
I’ve had a few requests for a sample project so I’m going to have to think about making the time to put one together. No free time for a few weeks tho. :-/
Thanks for this, Nick- good stuff!
One question: Is it necessary to create a copy of keyData? Seems like you could just use keyData directly in the call to -URLProtocol:didLoadData: rather than making a copy of it.
I do believe you could
@iOSDev: Try using 127.0.0.1 in place of localhost in your URLs. That seems to work better.
Oh, and sorry, Eric- I thanked Nick for the code, but I believe it was you who wrote the original article. Sorry about that, and thanks!
BTW, I did try this without making a copy of the NSData object, and it works just fine.
I originally had tried to register the app as the URL handler using the Info.plist file, but found that even though my app could be launched from Safari when the custom URL scheme was used, the app was not called to handle the requests from the player.
This method works great!
Hello!
I tried implementing my own NSURLProtocol, but AVPlayer refuses to even ask my protocol whether or not the URL can be handled by that protocol.
Here“s some code:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString * schemeString = request.URL.scheme;
NSLog(@”%@”,request.URL);
BOOL test = ([schemeString isEqualToString:MYURLSCHEMENAME);
return test;
}
NSLog never logs any URL here that is actually from the stream itself.
Any ideas?
Nevermind my previous post. Upon further inspection it actually did!
Hi,
Andry, I tried to subclass and get executed canInitWithRequest: method, but for me it doesn’t work. How did you managed to setup this environment properly? Can you share some code on github?
Regards,
Charles