Using Fairplay with iOS App
Pre-requisites​
To follow the instructions on this page, you will need:
- Access to any Apple developer membership account to download streaming SDK, not necessarily same as the one with FPS.
- Manifest URL for the encrypted HLS video.
- A license URL generated for testing. It is recommended to generate a longer validity URL for testing. But in production, this license URL should be fetched for each playback uniquely.
Downloading sample project​
- Go to the Apple's Fairplay streaming page: https://developer.apple.com/streaming/fps/
- Download the FairPlay Streaming Server SDK
- You will need a developer acount membership to download this file.
- Unzip the file will give you a sample xcode project at ``
├── Development
  ├── Client
    ├── FPS in TVML
    ├── FairPlay Streaming in Safari
    ├── HLS Catalog With FPS
├── Configuration
├── HLSCatalog <-- this is the target for iOS
├── HLSCatalog.xcodeproj
├── HLSCatalogTV
├── LICENSE.txt
├── README.md
├── Shared <-- important swift code here
    └── README.md
Adding specific information to this app.​
For this article, we will work with HLSCatalog target for iOS.
- Open
Shared/Resources/Streams.plist. - Add manifest URL to the
playlist_urlin third entry.. - Search for ADAPT markers in the app. There are two locations for that. Please find below code for replacing those two functions.
There are three two more errors added to make this work in enum.
enum ProgramError: Error {
case missingApplicationCertificate
case noCKCReturnedByKSM
case accessDenied
case networkError
}
requestApplicationCertificate​
func requestApplicationCertificate() throws -> Data {
print("request application certificate")
// this can also be bundled with the app to reduce load time
let certUrl = URL(string: "__________________")
var applicationCertificate: Data?
let semaphore = DispatchSemaphore(value: 0)
let dataTask = URLSession.shared.dataTask(with: certUrl!) {
applicationCertificate = $0; _ = $1; _ = $2
semaphore.signal()
}
dataTask.resume()
_ = semaphore.wait(timeout: .distantFuture)
guard applicationCertificate != nil else {
print("throw: missingApplicationCertificate")
throw ProgramError.missingApplicationCertificate
}
return applicationCertificate!
}
requestContentKeyFromKeySecurityModule​
func requestContentKeyFromKeySecurityModule(spcData: Data, assetID: String) throws -> Data {
var data: Data?
let semaphore = DispatchSemaphore(value: 0)
let licenseUrl = URL(string: "_______________")
var request = URLRequest(url: licenseUrl!)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/octet-stream", forHTTPHeaderField: "Accept")
let licenseReqObj = ["spc" :spcData.base64EncodedString(), "assetId" : ""]
do {
let parameters = licenseReqObj
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
}
var httpResponse: URLResponse?
let dataTask = URLSession.shared.dataTask(with: request as URLRequest) {
data = $0; httpResponse = $1; _ = $2
semaphore.signal()
}
dataTask.resume()
_ = semaphore.wait(timeout: .distantFuture)
guard data != nil else {
print("throw: network failed")
throw ProgramError.networkError
}
guard let response = httpResponse as? HTTPURLResponse,
(response.statusCode == 200) else {
print("throw: license not returned.")
// print("throw: license not returned: " + (responseObj??["message"]! as! String))
throw ProgramError.accessDenied
}
return data!
}