Mastering FairPlay DRM: Key Management, Prewarming, and Offline Playback on iOS

This article explains DRM fundamentals, the FairPlay workflow, key management methods using AVAssetResourceLoader and AVContentKeySession, tips for handling codec tags, and step‑by‑step guidance for downloading HLS videos with AVAssetDownloadTask and AVAggregateAssetDownloadTask on iOS.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Mastering FairPlay DRM: Key Management, Prewarming, and Offline Playback on iOS

01 DRM Introduction

DRM, Digital Rights Management, refers to using encryption technology to protect video content, securely store and transmit keys (encryption and decryption keys), and allow content producers to set commercial rules that restrict viewers.

1.1 DRM Workflow

DRM uses symmetric‑key algorithms to encrypt video content; the same key encrypts and decrypts.

The content is first encrypted (usually with AES‑128) and then delivered to the client. The key is provided by a dedicated server.

When the client wants to play the encrypted video, it requests a decryption key from the DRM server.

The server authenticates the client; if authentication succeeds, the server sends the decryption key and license rules.

After receiving the key, the client uses a Content Decryption Module (CDM) to decrypt, decode, and render the video securely.

1.2 DRM Schemes

Common DRM schemes include several options; on Apple platforms the FairPlay scheme is used.

FairPlay supported protocols :

We use the HLS + fmp4 solution.

FairPlay supported platforms and system requirements :

FairPlay playback DRM video flow

User taps play, passes a .m3u8 URL to AVPlayer.

Player downloads and parses the m3u8 playlist, finds #EXT-X-KEY, indicating encrypted video.

Request SPC (Secure Playback Context) from the system.

Request CKC (Content Key Context) from the backend; the key server uses the SPC to locate the content key and returns it in CKC.

AVFoundation receives CKC, uses the key to decrypt and decode the video, then continues playback.

Glossary

SPC (Secure Playback Context): encrypted key request information.

CKC (Content Key Context): encrypted key response containing the decryption key and its validity period.

KSM (Key Security Module): backend module handling keys.

CDM (Content Decryption Module): client‑side module that decrypts video using AVPlayer after receiving CKC.

.m3u8 EXT‑X‑KEY example :

#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://12341234123412341234123412341234?iv=12341234123412341234123412341234"
#EXTINF:10.0,
seg-1.m4s
...
#EXTINF:2.0,
seg-35.m4s
#EXT-X-ENDLIST

Sequence diagram from FPS official documentation :

1.3 Tip1: On Apple platforms HLS fmp4 fragment tag should be hvc1

hev1

and hvc1 are two codec tags representing different packaging of HEVC streams in an MP4 container. QuickTime Player and iOS do not support the hev1 tag. If an MP4 uses hev1, playback will show audio only, no video.

02 Key Management Methods

The previous section mentioned that playing FairPlay video requires obtaining the correct decryption key; otherwise playback fails or shows a green screen.

Apple provides two ways to manage FairPlay keys:

Use AVAssetResourceLoader Use

AVContentKeySession

2.1 Method 1: Using AVAssetResourceLoader

This method requests the key only after the user taps play, so the first‑frame latency can be high.

Specific usage:

Obtain the AVAssetResourceLoader from [self.urlAsset resourceLoader] and set its delegate.

[[self.urlAsset resourceLoader] setDelegate:loaderDelegate queue:globalNotificationQueue()];

Create a class that implements AVAssetResourceLoaderDelegate and its resourceLoader:shouldWaitForRenewalOfRequestedResource: method.

Request SPC from iOS, then request CKC from the backend.

// Request SPC
NSData *requestBytes = [loadingRequest streamingContentKeyRequestDataForApp:certificate contentIdentifier:assetId options:nil error:&error];
// Send SPC to server and get CKC
NSData *responseData = [SofaAVContentKeyManager getlicenseWithSpcData:requestBytes contentIdentifierHost:assetStr leaseExpiryDuration:&expiryDuration fpRedemptionUrl:fpRedemptionUrl error:&error];
if (responseData) {
    [dataRequest respondWithData:responseData];
    [loadingRequest finishLoading];
} else {
    [loadingRequest finishLoadingWithError:error];
}
SofaAssetLoaderDelegate *loaderDelegate = [[SofaAssetLoaderDelegate alloc] init];
loaderDelegate.fpCerData = self.fpCerData;
loaderDelegate.fpRedemptionUrl = fpRedemption;
loaderDelegate.asset = self.urlAsset;
[[self.urlAsset resourceLoader] setDelegate:loaderDelegate queue:globalNotificationQueue()];
[self.urlAsset loadValuesAsynchronouslyForKeys:requestedKeys completionHandler:^{
    dispatch_async(dispatch_get_main_queue(), ^{
        AVPlayerItem *newItem = [AVPlayerItem playerItemWithAsset:weakSelf.urlAsset automaticallyLoadedAssetKeys:keys];
        [weakSelf.player replaceCurrentItemWithPlayerItem:newItem];
    });
}];

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForRenewalOfRequestedResource:(AVAssetResourceRenewalRequest *)renewalRequest {
    return [self resourceLoader:resourceLoader shouldWaitForLoadingOfRequestedResource:renewalRequest];
}

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
    // Extract URL, request SPC, send to server, handle CKC, etc.
    // (Implementation omitted for brevity)
    return YES;
}

2.2 Method 2: Using AVContentKeySession

This API, introduced in 2017, decouples key management from playback and supports pre‑warming and offline playback.

Simple usage of AVContentKeySession

Create a session and set its delegate.

_keyQueue = dispatch_queue_create("com.sohuvideo.contentkeyqueue", DISPATCH_QUEUE_SERIAL);
self.keySession = [AVContentKeySession contentKeySessionWithKeySystem:AVContentKeySystemFairPlayStreaming];
[self.keySession setDelegate:self queue:_keyQueue];

Implement delegate methods.

- (void)contentKeySession:(AVContentKeySession *)session didProvideContentKeyRequest:(AVContentKeyRequest *)keyRequest {
    [self handleStreamingContentKeyRequest:keyRequest];
}

- (void)contentKeySession:(AVContentKeySession *)session didProvideRenewingContentKeyRequest:(AVContentKeyRequest *)keyRequest {
    // Handle renewal if needed
}

- (void)contentKeySession:(AVContentKeySession *)session didProvidePersistableContentKeyRequest:(AVPersistableContentKeyRequest *)keyRequest {
    [self handlePersistableContentKeyRequest:keyRequest];
}

- (void)contentKeySession:(AVContentKeySession *)session contentKeyRequest:(AVContentKeyRequest *)keyRequest didFailWithError:(NSError *)err {
    // Handle error
}

Add the URLAsset to the session.

[self.keySession addContentKeyRecipient:recipient];

Three usage scenarios

Scenario 1 – No pre‑warming : Request the key after the user taps play, similar to the AVAssetResourceLoader approach. First‑frame latency may be larger.

// Add URLAsset to session
[self.keySession addContentKeyRecipient:recipient];
NSURL *assetUrl = [NSURL URLWithString:dataSource.path];
self.urlAsset = (AVURLAsset *)[AVAsset assetWithURL:assetUrl];
NSArray *requestedKeys = @"playable";
[self.urlAsset loadValuesAsynchronouslyForKeys:requestedKeys completionHandler:^{
    dispatch_async(dispatch_get_main_queue(), ^{
        AVPlayerItem *newItem = [AVPlayerItem playerItemWithAsset:weakSelf.urlAsset automaticallyLoadedAssetKeys:keys];
        [weakSelf.player replaceCurrentItemWithPlayerItem:newItem];
    });
}];

- (void)contentKeySession:(AVContentKeySession *)session didProvideContentKeyRequest:(AVContentKeyRequest *)keyRequest {
    [self handleStreamingContentKeyRequest:keyRequest];
}

Scenario 2 – Pre‑warming : Predict the next video, request its key in advance to reduce first‑frame time.

[self.keySession processContentKeyRequestWithIdentifier:asset.contentId initializationData:nil options:nil];
// The rest of the flow is the same as Scenario 1.

Scenario 3 – Offline playback : Request a persistable key before download, store it locally, and use it when playing without network.

Request persistable key.

[keyRequest respondByRequestingPersistableContentKeyRequestAndReturnError:&err];

Store the key.

[contentKey writeToURL:fileUrl options:NSDataWritingAtomic error:&err];

During playback, if the key exists locally, use it; otherwise request a new one and update the stored key.

- (void)contentKeySession:(AVContentKeySession *)session didProvideContentKeyRequest:(AVContentKeyRequest *)keyRequest {
    if ([self.pendingPersistableContentKeyIdentifiers containsObject:assetID] || [self persistableContentKeyExistsOnDiskWithContentKeyIdentifier:assetID]) {
        [keyRequest respondByRequestingPersistableContentKeyRequestAndReturnError:&err];
        // Use stored key or request new one
    } else {
        [self handleStreamingContentKeyRequest:keyRequest];
    }
}

Persistable keys have two expiration concepts:

Storage Duration : The period the key can stay on the device before playback; typically long (e.g., 30 days).

Playback Duration : The period the key is valid after playback starts; typically short (e.g., 48 hours). The system updates the key via

contentKeySession:didUpdatePersistableContentKey:forContentKeyIdentifier:

.

2.3 Tip1: Use fileURLWithPath for local media URLs

let fileUrl = NSURL.fileURL(withPath: "/Library/Caches/aHR0cDovLzEwLjI==_E0363AAE664D0C7E.movpkg")
let urlAsset = AVAsset(url: fileUrl as URL)

2.4 Tip2: Manage AVContentKeySession with a singleton

@interface SofaAVContentKeyManager () <AVContentKeySessionDelegate>
@property (nonatomic, strong, readwrite) AVContentKeySession *keySession;
@end

+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static SofaAVContentKeyManager *instance;
    dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; });
    return instance;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        [self createKeySession];
    }
    return self;
}

- (void)createKeySession {
    _keyQueue = dispatch_queue_create("com.sohuvideo.contentkeyqueue", DISPATCH_QUEUE_SERIAL);
    self.keySession = [AVContentKeySession contentKeySessionWithKeySystem:AVContentKeySystemFairPlayStreaming];
    [self.keySession setDelegate:self queue:_keyQueue];
}

03 Video Download AVAssetDownloadTask

3.1 Download HLS video with AVAssetDownloadTask

Steps:

Create an AVAssetDownloadURLSession instance.

let hlsAsset = AVURLAsset(url: assetURL)
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "assetDownloadConfigurationIdentifier")
let assetDownloadURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)

Create and start the download task.

guard let downloadTask = assetDownloadURLSession.makeAssetDownloadTask(asset: asset.urlAsset, assetTitle: asset.stream.name, assetArtworkData: nil, options: nil) else { return }
downloadTask.taskDescription = asset.stream.name
downloadTask.resume()

Implement AVAssetDownloadDelegate callbacks.

// Path determined
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, willDownloadTo location: URL) {
    print("Will download to", location.path)
}

// Progress update
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
    var percentComplete = 0.0
    for value in loadedTimeRanges {
        let loaded = value.timeRangeValue
        percentComplete += CMTimeGetSeconds(loaded.duration) / CMTimeGetSeconds(timeRangeExpectedToLoad.duration)
    }
    print("Download progress:", percentComplete)
}

// Completion
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
    print("File downloaded to", location.path)
}

// Task finished
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) {
    guard let task = task as? AVAssetDownloadTask else { return }
    if let error = error as NSError? {
        switch (error.domain, error.code) {
        case (NSURLErrorDomain, NSURLErrorCancelled):
            print("User cancelled")
        case (NSURLErrorDomain, NSURLErrorUnknown):
            fatalError("Simulator does not support HLS download")
        default:
            fatalError("Error occurred \(error.domain)")
        }
    } else {
        print("Task complete")
    }
}

3.2 Tip1 – Download path cannot be set manually; move file after completion

func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
    let cachePath = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!.appending("xxx.movpkg"))
    move(file: location, to: cachePath)
}

func move(file: URL, to destinationPath: URL) {
    guard FileManager.default.fileExists(atPath: file.path) else { print("Source file does not exist."); return }
    do {
        if FileManager.default.fileExists(atPath: destinationPath.path) {
            try FileManager.default.removeItem(at: destinationPath)
        }
        try FileManager.default.moveItem(at: file, to: destinationPath)
    } catch {
        print("Error moving file: \(error)")
    }
}

3.3 Tip2 – Downloaded file is a .movpkg bundle

Both MP4 and HLS downloads produce a file ending with .movpkg. Only HLS .movpkg is a bundle that can be played directly with AVPlayer or AVPlayerViewController. For MP4, rename the file to .mp4 after moving it to a playable location.

3.4 Tip3 – Use AVAggregateAssetDownloadTask for multi‑track HLS

If the HLS stream contains multiple bitrates, audio tracks, or subtitles, use AVAggregateAssetDownloadTask to download specific media selections.

let preferredMediaSelection = asset.urlAsset.preferredMediaSelection
guard let task = assetDownloadURLSession.aggregateAssetDownloadTask(with: asset.urlAsset, mediaSelections: [preferredMediaSelection], assetTitle: asset.stream.name, assetArtworkData: nil, options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 265_000]) else { return }

task.taskDescription = asset.stream.name
task.resume()

Delegate callbacks for aggregate tasks are:

func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, willDownloadTo location: URL) { }
func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange, for mediaSelection: AVMediaSelection) { }
func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, didCompleteFor mediaSelection: AVMediaSelection) { }

04 Reference Links

Apple Developer, FairPlay Streaming (https://developer.apple.com/streaming/fps/)

WWDC 2018, AVContentKeySession Best Practices (https://devstreaming-cdn.apple.com/videos/wwdc/2018/507axjplrd0yjzixfz/507/507_hd_avcontentkeysession_best_practices.mp4?dl=1)

WWDC 2020, Discover how to download and play HLS offline (https://developer.apple.com/videos/play/wwdc2020/10655/)

iOShlsDRMKey ManagementFairPlayVideo DownloadAVContentKeySession
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.