Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ios): support DRM-encrypted video assets #13844

Merged
merged 7 commits into from Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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