Skip to content

Instantly share code, notes, and snippets.

@fousa
Last active June 1, 2023 12:28
Show Gist options
  • Save fousa/5709fb7c84e5b53dbdae508c9cb4fadc to your computer and use it in GitHub Desktop.
Save fousa/5709fb7c84e5b53dbdae508c9cb4fadc to your computer and use it in GitHub Desktop.
Integrate HLS with FairPlay.
class FairPlayer: AVPlayer {
private let queue = DispatchQueue(label: "com.icapps.fairplay.queue")
func play(asset: AVURLAsset) {
// Set the resource loader delegate to this class. The `resourceLoader`'s delegate will be
// triggered when FairPlay handling is required.
asset.resourceLoader.setDelegate(self, queue: queue)
// Load the asset in the player.
let item = AVPlayerItem(asset: asset)
// Set the current item in this player instance.
replaceCurrentItem(with: item)
// Start playing the item. From the moment the `play` is triggered the `resourceLoader` will
// do the rest of the work.
play()
}
}
extension FairPlayer: AVAssetResourceLoaderDelegate {
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
// We first check if a url is set in the manifest.
guard let url = loadingRequest.request.url else {
print("🔑", #function, "Unable to read the url/host data.")
loadingRequest.finishLoading(with: NSError(domain: "com.icapps.error", code: -1, userInfo: nil))
return false
}
print("🔑", #function, url)
// When the url is correctly found we try to load the certificate date. Watch out! For this
// example the certificate resides inside the bundle. But it should be preferably fetched from
// the server.
guard
let certificateURL = Bundle.main.url(forResource: "certificate", withExtension: "der"),
let certificateData = try? Data(contentsOf: certificateURL) else {
print("🔑", #function, "Unable to read the certificate data.")
loadingRequest.finishLoading(with: NSError(domain: "com.icapps.error", code: -2, userInfo: nil))
return false
}
// Request the Server Playback Context.
let contentId = "hls.icapps.com"
guard
let contentIdData = contentId.data(using: String.Encoding.utf8),
let spcData = try? loadingRequest.streamingContentKeyRequestData(forApp: certificateData, contentIdentifier: contentIdData, options: nil),
let dataRequest = loadingRequest.dataRequest else {
loadingRequest.finishLoading(with: NSError(domain: "com.icapps.error", code: -3, userInfo: nil))
print("🔑", #function, "Unable to read the SPC data.")
return false
}
// Request the Content Key Context from the Key Server Module.
let ckcURL = URL(string: "https://hls.icapps.com/ckc")!
var request = URLRequest(url: ckcURL)
request.httpMethod = "POST"
request.httpBody = spcData
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: request) { data, response, error in
if let data = data {
// The CKC is correctly returned and is now send to the `AVPlayer` instance so we
// can continue to play the stream.
dataRequest.respond(with: data)
loadingRequest.finishLoading()
} else {
print("🔑", #function, "Unable to fetch the CKC.")
loadingRequest.finishLoading(with: NSError(domain: "com.icapps.error", code: -4, userInfo: nil))
}
}
task.resume()
return true
}
}
@sahilchaddha
Copy link

Hi, I have been able to use this snippet for my Player. It works for ios 11. But breaks for ios 10(10.3.3). AVPlayer throws error

Error Domain=AVFoundationErrorDomain Code=-11819 \"Cannot Complete Action\" UserInfo={NSLocalizedDescription=Cannot Complete Action, NSLocalizedRecoverySuggestion=Try again later.})

@amirpervaiz086
Copy link

Thanks :)

@amirpervaiz086
Copy link

I found apple has changed implementation in their sample project by using AVContentKey (*iOS 10.3.3) before AVAssetResourceLoaderDelegate was being used. But I would stick with AVAssetResourceLoader implementation.

@mangaloreglitz
Copy link

I have been trying to implement the FairPlay streaming DRM in my project. I'm struck in getting the Content from the CKC data. I'm getting the below errors.
Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-16152), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x10bc54770 {Error Domain=NSOSStatusErrorDomain Code=-16152 "(null)"}}
Error Domain=AVFoundationErrorDomain Code=-11835 "Cannot Open" UserInfo={NSUnderlyingError=0x17025c860 {Error Domain=NSOSStatusErrorDomain Code=-42681 "(null)"}, NSLocalizedFailureReason=This content is not authorized., NSLocalizedDescription=Cannot Open}

How to validate the implementation step by step?

Thanks

@jovanpreet
Copy link

Same here. It works on ios11 but fails on ios10. any solution?

@veereshkumbargs
Copy link

can u provide any demo url asset so that i can pass it as a argument to the FairPlayer.play() method

@xtabbas
Copy link

xtabbas commented Nov 2, 2018

So setting delegate using asset.resourceLoader.setDelegate(self, queue: queue) automatically invokes the resourceLoader method? I am new to swift and I am still trying to wrap my head around how that works.

@inickt
Copy link

inickt commented Jan 4, 2019

Have you had any luck doing something like this with AirPlay?

@abesmon
Copy link

abesmon commented Mar 29, 2019

It's seems that this demo is not working now. I found out, that Apple's demo is better showing how to make this works:
https://developer.apple.com/services-account/download?path=/Developer_Tools/FairPlay_Streaming_Server_SDK/FairPlay_Streaming_Server_SDK_v4.2.0.zip

For me it was an issue with line #49, where streamingContentKeyRequestData appeared. I dont know exactly, but it seems that calling this function took too many time, so task fails before it could resolve.

In apple's demo they wrap all this async stuff in async block on separate queue, and it works well for me. I wrapped everything inside resourceLoader(...) in async block except return true and now it works

@tiwari1amrit
Copy link

tiwari1amrit commented Apr 2, 2019

after implementing the above methods, I got data with empty size then I called finishedLoading() method by implementing dataRequest.respond(with: data) and got below error from AVPlayer.
Error Domain=AVFoundationErrorDomain Code=-11835 "Cannot Open" UserInfo={NSLocalizedFailureReason=This content is not authorized., NSLocalizedDescription=Cannot Open, NSUnderlyingError=0x106f2afa0 {Error Domain=NSOSStatusErrorDomain Code=-42681 "(null)"} from the AVPlayer.
I am confuse on how to get contentId, which is used to create content Identifier. Can anybody help me?

@tiwari1amrit
Copy link

@mangaloreglitz Have you got the solution?

@linkmanishgupta
Copy link

What is specifically "contentId " here?
Can anybody please throw some light?

@yksingh12
Copy link

What is specifically "contentId " here?
Can anybody please throw some light?

Any luck here?

@lkuraer
Copy link

lkuraer commented Mar 15, 2021

@yksingh12 @linkmanishgupta for me it was URI that is provided in m3u8 manifest, for example:

#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://contentId",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"

@rafaelreis-hotmart
Copy link

rafaelreis-hotmart commented Jun 2, 2021

What is specifically "contentId " here?

contentId or AssetId depends on your DRM supplier. I saw some implementations that uses some Identifier for the media. It could be the media code, for example. I saw some implementations using a key inside this URL skd://whatever. Found the Docs about how to integrate with your supplier.

@amitsingh12ap
Copy link

content id is host url "contentId = url.host"

@15kiortiz
Copy link

@yksingh12 @linkmanishgupta for me it was URI that is provided in m3u8 manifest, for example:

#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://contentId",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"

where could I edit this? I want to be able to test the fairplay. Thank you very much

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment