Play DRM-protected content (iOS)

Learn how to enable digital rights management (DRM) protected playback for an iOS app.



FairPlay Streaming (FPS) securely delivers keys to Apple mobile devices, enabling the playback of encrypted video content. FPS allows a content provider to securely deliver an AES 128-bit content key from the provider’s key server. The content provider encrypts the H.264 video content with the content key. Then, FPS decrypts the encrypted video content with the content key.

πŸ“˜

The JWPlayerKit does not provide the following:

  • Encrypt streams
  • Implement a key server
  • Support offline DRM

The SDK (JWPlayerKit) detects when a stream is FairPlay-encrypted and asks your app to provide the necessary information and key to decrypt the content. This approach gives you the flexibility to encrypt your content and manage your keys the way you prefer.



FPS Compliance

In order to be FPS-compliant, you must complete the following tasks:

  • Write a Key Security Module that is installed in a key server's software.
  • Add code that enables your app to request the content decryption key from your key server.
  • Create the formatting and encryption software for the media content server. This software prepares the encrypted content stream according to the Apple HTTP Live Streaming (HLS) specification.


Implementation

πŸ“˜

FairPlay decryption only works on a physical device and will not work on a simulator.

Use the following steps to set up DRM playback in your app:

  1. Create a JWPlayerViewController.

  2. Load a configuration with the protected stream file.

    let url = URL(string: "protected-stream.m3u8")!
    let playerItem = try! JWPlayerItemBuilder().file(url).build()
    let config = try! JWPlayerConfigurationBuilder().playlist([playerItem]).build()
     
    player.contentKeyDataSource = self
    player.configurePlayer(with: config)
    
    NSError *error;
    JWPlayerItemBuilder *builder = [[JWPlayerItemBuilder alloc] init];
    [builder file:[NSURL URLWithString:@"protected-stream.m3u8"]];
    JWPlayerItem *item = [builder buildAndReturnError:&error];
    JWPlayerConfigurationBuilder *builder = [[JWPlayerConfigurationBuilder alloc] init];
    JWPlayerConfiguration *config = [builder buildAndReturnError:&error];
     
    player.contentKeyDataSource = self;
    [player configurePlayerWith:config];
    
  3. Conform to JWDRMContentKeyDataSource protocol in the class that you would like to handle the data source logic for playing protected content.

  4. Implement the contentIdentifierForURL(_ url: URL, completionHandler: (_ contentIdentifier: Data?) -> Void) delegate method in your class.

    When called, this delegate method requests the identifier for the protected content to be passed through the delegate method's completion block.

    func contentIdentifierForURL(_ url: URL, completionHandler handler: @escaping (Data?) -> Void) {
        let uuid = "content-uuid"
        let uuidData = uuid.data(using: .utf8)
        handler(uuidData)
    }
    
    - (void)contentIdentifierForURL:(NSURL * _Nonnull)url completionHandler:(void (^ _Nonnull)(NSData * _Nullable))handler {
        NSString *contentUUID = @"content-uuid";
        NSData *uuidData = [contentUUID dataUsingEncoding:NSUTF8StringEncoding];
        handler(uuidData);
    }
    
  5. Implement the appIdentifierForURL(_ url: URL, completionHandler: (Data?) -> Void) delegate method in your class.

    When called, this delegate method requests an Application Certificate binary which must be passed through the completion block.

    πŸ“˜

    The Application Certificate must be encoded with the X.509 standard with distinguished encoding rules (DER). It is obtained when registering an FPS playback app with Apple, by supplying an X.509 Certificate Signing Request linked to your private key.

    func appIdentifierForURL(_ url: URL, completionHandler handler: @escaping (Data?) -> Void) {
        guard let certUrl = Bundle.main.url(forResource: "fps", withExtension: "cer"),
              let appIdData = try? Data(contentsOf: certUrl) else {
            handler(nil)
            return
        }
        handler(appIdData)
    }
    
    - (void)appIdentifierForURL:(NSURL * _Nonnull)url completionHandler:(void (^ _Nonnull)(NSData * _Nullable))handler {
        NSBundle *bundle = [NSBundle mainBundle];
        NSURL *certURL = [bundle URLForResource:@"fps" withExtension:@"cer"];
        NSData *certData = [NSData dataWithContentsOfURL:certURL];
        handler(certData);
    }
    
  6. Implement the contentKeyWithSPCData(_ spcData: Data, completionHandler: (Data?, Date?, String?) -> Void) delegate method in your class.

    func contentKeyWithSPCData(_ spcData: Data, completionHandler handler: @escaping (Data?, Date?, String?) -> Void) {
        let currentTime = Date().timeIntervalSince1970
        let spcProcessURL = "SPC-PROCESS-URL".appendingFormat("/%@?p1=%li", "content-uuid", currentTime)
        var ckcRequest = URLRequest(url: URL(string: spcProcessURL)!)
        ckcRequest.httpMethod = "POST"
        ckcRequest.httpBody = spcData
        ckcRequest.addValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
     
     
        URLSession.shared.dataTask(with: ckcRequest) { (data, response, error) in
            guard error == nil, let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                handler(nil, nil, nil)
                return
            }
            handler(data, nil, nil)
        }.resume()
    }
    
    - (void)contentKeyWithSPCData:(NSData * _Nonnull)spcData completionHandler:(void (^ _Nonnull)(NSData * _Nullable, NSDate * _Nullable, NSString * _Nullable))handler {
        NSTimeInterval currentTime = [[NSDate alloc] timeIntervalSince1970];
        NSString *spcProcessURL = [NSString stringWithFormat:@"SPC-PROCESS-URL/%@?p1=%li", @"content-uuid", (NSInteger)currentTime];
        NSMutableURLRequest *ckcRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:spcProcessURL]];
        [ckcRequest setHTTPMethod:@"POST"];
        [ckcRequest setHTTPBody:spcData];
        [ckcRequest addValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
     
        [[[NSURLSession sharedSession] dataTaskWithRequest:ckcRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
            if (error != nil || (httpResponse != nil && httpResponse.statusCode != 200)) {
                handler(nil, nil, nil);
                return;
            }
     
            handler(data, nil, nil);
        }] resume];
    }
    

When the key request is ready, this delegate method provides the key request data (SPC - Server Playback Context message) needed to retrieve the Content Key Context (CKC) message from your key server. The CKC message must be returned via the completion block under the response parameter.

After your app sends the request to the server, the FPS code on the server sends the required key to the app. This key is wrapped in an encrypted message. Your app provides the encrypted message to the JWPlayerKit. The JWPlayerKit unwraps the message and decrypts the stream to enable playback on an iOS device.


πŸ“˜

For resources that may expire, specify a renewal date in the completion block and the content type.