Skip to content

Commit

Permalink
feat(ios): support DRM-encrypted video assets (#13844)
Browse files Browse the repository at this point in the history
* chore: check-in current wip

* chore: update error handling, add debug logs

* feat: finish initial implementation

* fix: remove unused logs

* feat: add docs
  • Loading branch information
hansemannn committed Aug 10, 2023
1 parent c7154f7 commit d4cd040
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 4 deletions.
24 changes: 24 additions & 0 deletions apidoc/Titanium/Media/VideoPlayer.yml
Expand Up @@ -484,6 +484,17 @@ properties:
type: Number
default: 0

- name: fairPlayConfiguration
platforms: [iphone, ipad, macos]
summary: 'Handle DRM-encrypted video assets using the [Apple FairPlay Streaming API](https://developer.apple.com/streaming/fps/).'
description:
Secure the delivery of streaming media to devices through the HTTP Live Streaming (HLS) protocol. Using FairPlay Streaming (FPS)
technology, content providers, encoding vendors, and delivery networks can encrypt content, securely exchange keys,
and protect playback on iOS, iPadOS and macOS.
type: FairPlayConfiguration
availability: creation
since: "12.2.0"

- name: fullscreen
platforms: [android]
deprecated:
Expand Down Expand Up @@ -711,3 +722,16 @@ properties:
- name: height
summary: Height of the movie.
type: Number

---
name: FairPlayConfiguration
summary: An object representing a FairPlay Streaming configuration.
platforms: [iphone, ipad, macos]
properties:
- name: licenseURL
summary: The FairPlay Streaming license URL to handle the server-side authentication flow.
type: String

- name: certificate
summary: The FairPlay Streaming public certificate to authenticate the content key request.
type: Titanium.Blob
5 changes: 4 additions & 1 deletion iphone/Classes/TiMediaVideoPlayerProxy.h
Expand Up @@ -13,7 +13,7 @@
#import <TitaniumKit/TiFile.h>
#import <TitaniumKit/TiViewProxy.h>

@interface TiMediaVideoPlayerProxy : TiViewProxy {
@interface TiMediaVideoPlayerProxy : TiViewProxy <AVAssetResourceLoaderDelegate> {
@protected
AVPlayerViewController *movie;
AVPlayerItem *item;
Expand Down Expand Up @@ -42,6 +42,9 @@

// Track the playback state for parity
TiVideoPlayerPlaybackState _playbackState;

NSData *fairPlayCertificate;
NSString *fairPlayLicenseURL;
}

@property (nonatomic, readwrite, assign) id url;
Expand Down
78 changes: 75 additions & 3 deletions iphone/Classes/TiMediaVideoPlayerProxy.m
Expand Up @@ -53,7 +53,7 @@ @implementation TiMediaVideoPlayerProxy
- (NSArray *)keySequence
{
if (moviePlayerKeys == nil) {
moviePlayerKeys = [[NSArray alloc] initWithObjects:@"url", nil];
moviePlayerKeys = [[NSArray alloc] initWithObjects:@"fairPlayConfiguration", @"url", nil];
}
return moviePlayerKeys;
}
Expand Down Expand Up @@ -144,7 +144,15 @@ - (AVPlayerViewController *)ensurePlayer
return nil;
}
movie = [[AVPlayerViewController alloc] init];
[movie setPlayer:[AVPlayer playerWithURL:url]];
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:nil];

// Apply DRM validation if configuration is provided
if (fairPlayCertificate != nil) {
[urlAsset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
}

AVPlayerItem *newVideoItem = [AVPlayerItem playerItemWithAsset:urlAsset];
[movie setPlayer:[AVPlayer playerWithPlayerItem:newVideoItem]];
[self configurePlayer];
}
[playerLock unlock];
Expand Down Expand Up @@ -194,6 +202,20 @@ - (void)windowWillClose

#pragma mark Public APIs

- (void)setFairPlayConfiguration:(id)params
{
ENSURE_SINGLE_ARG(params, NSDictionary);

NSLog(@"[WARN] Setting \"fairPlayConfiguration\" property …");

fairPlayLicenseURL = [[TiUtils stringValue:@"licenseURL" properties:params] retain];
fairPlayCertificate = [[(TiBlob *)params[@"certificate"] data] retain];

if (fairPlayCertificate == nil || fairPlayLicenseURL == nil) {
[self throwException:@"Invalid method call!" subreason:@"Missing certificate, callback or fairPlayLicenseURL" location:CODELOCATION];
}
}

- (void)setOverlayView:(id)proxy
{
if (movie != nil && [movie view] != nil) {
Expand Down Expand Up @@ -340,7 +362,14 @@ - (void)setUrl:(id)url_
loaded = NO;
sizeSet = NO;
if (movie != nil) {
AVPlayerItem *newVideoItem = [AVPlayerItem playerItemWithURL:url];
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:nil];

// Apply DRM validation if configuration is provided
if (fairPlayCertificate != nil) {
[urlAsset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
}

AVPlayerItem *newVideoItem = [AVPlayerItem playerItemWithAsset:urlAsset];
[[movie player] replaceCurrentItemWithPlayerItem:newVideoItem];
[self removeNotificationObserver]; // Remove old observers
[self addNotificationObserver]; // Add new oberservers
Expand Down Expand Up @@ -986,6 +1015,49 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
}
}

#pragma mark AVAssetResourceLoaderDelegate

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)authenticationChallenge
{
NSLog(@"[DEBUG] Cancelled resource loader authentication challenge");
}

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
NSString *contentId = loadingRequest.request.URL.host;

if (contentId) {
NSError *error;
NSData *contentIdData = [contentId dataUsingEncoding:NSUTF8StringEncoding];
NSData *spcData = [loadingRequest streamingContentKeyRequestDataForApp:fairPlayCertificate contentIdentifier:contentIdData options:nil error:&error];

if (error == nil) {
NSMutableURLRequest *ckcRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:fairPlayLicenseURL]];
[ckcRequest setHTTPMethod:@"POST"];
[ckcRequest setHTTPBody:spcData];

NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:ckcRequest
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (data) {
[loadingRequest.dataRequest respondWithData:data];
[loadingRequest finishLoading];
} else {
[loadingRequest finishLoadingWithError:error];
}
}];

[task resume];

return YES;
} else {
[loadingRequest finishLoadingWithError:error];
}
}

return NO;
}

@end

#endif

0 comments on commit d4cd040

Please sign in to comment.