For iOS17 we’ve had no problem playing Apple Fairplay encrypted content with keys delivered from our key server running on FairPlay Streaming Server SDK 5.1 and subsequently FairPlay Streaming Server SDK 26. It’s built and deployed using **Xcode Version 26.1.**1 (17B100) with no changes to the code and – as expected – the content continued to be successfully decrypted and played (so far so good). However, as soon as a device was updated to iOS26, that device would no longer play the encrypted content.
Devices remaining on iOS17 continue to work normally and the debugging logs are a sanity-check that proves that. Is anyone else experiencing this issue? We’ve raised this issue several times with Apple over the past two months and are yet to get a response from their engineers or the developer forum.
Here’s the code (you should be able to drop it into a fresh iOS Xcode project and provide a server url, content url and certificate). Many thanks in advance and to those who have already responded to my poor first attempt at asking for help.
import UIKit
import AVFoundation
class ViewController: UIViewController {
private var player: AVPlayer?
private var keyDelegate: ContentKeyDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .orange
self.configureAudioSession()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate
, let hlsURL = appDelegate.hlsURL()
, let certificateData = appDelegate.certificate()
, let licenseServerURL = appDelegate.licenseServerURL() else {
return
}
let delegate = ContentKeyDelegate(appCertificate: certificateData,licenseServerURL: licenseServerURL)
self.keyDelegate = delegate
let asset = AVURLAsset(url: hlsURL, options: [
AVURLAssetPreferPreciseDurationAndTimingKey: NSNumber(value: false)
])
delegate.setAsset(asset)
let item = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: item)
player.automaticallyWaitsToMinimizeStalling = true
player.allowsExternalPlayback = false
player.preventsDisplaySleepDuringVideoPlayback = true
self.player = player
let layer = AVPlayerLayer(player: player)
layer.frame = view.bounds
layer.videoGravity = .resizeAspect
self.view.layer.addSublayer(layer)
player.play()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let playerLayer = view.layer.sublayers?.first(where: { $0 is AVPlayerLayer }) as? AVPlayerLayer {
playerLayer.frame = view.bounds
}
}
private func configureAudioSession() {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .moviePlayback, options: [])
try audioSession.setActive(true)
print("✅ Audio session configured successfully")
} catch {
print("❌ Failed to configure audio session: \(error)")
}
}
}
final class ContentKeyDelegate: NSObject, AVContentKeySessionDelegate {
private let appCertificate: Data
private let licenseServerURL: URL
private let keySession: AVContentKeySession
init(appCertificate: Data, licenseServerURL: URL) {
self.appCertificate = appCertificate
self.licenseServerURL = licenseServerURL
self.keySession = AVContentKeySession(keySystem: .fairPlayStreaming)
super.init()
self.keySession.setDelegate(self, queue: DispatchQueue.main)
}
func setAsset(_ asset: AVURLAsset) {
self.keySession.addContentKeyRecipient(asset)
}
func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) {
process(keyRequest: keyRequest)
}
func contentKeySession(_ session: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) {
process(keyRequest: keyRequest)
}
private func process(keyRequest: AVContentKeyRequest) {
guard let assetIDString = keyRequest.identifier as? String, let assetIDData = assetIDString.data(using: .utf8) else {
keyRequest.processContentKeyResponseError(NSError(domain: "ContentKey", code: -1))
return
}
Task {
do {
let spcData = try await keyRequest.makeStreamingContentKeyRequestData(forApp: appCertificate,contentIdentifier: assetIDData, options: nil)
self.fetchCKC(spcData: spcData, assetId: assetIDString) { result in
switch result {
case .success(let ckcOpt):
guard let ckcData = ckcOpt else {
DispatchQueue.main.async {
keyRequest.processContentKeyResponseError(ProgramError.noCKCReturnedByKSM)
}
return
}
let response = AVContentKeyResponse(fairPlayStreamingKeyResponseData: ckcData)
DispatchQueue.main.async {
keyRequest.processContentKeyResponse(response)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
if let err = keyRequest.error {
Swift.print("keyRequest.error: \(err.localizedDescription) / \(err)")
} else {
Swift.print("keyRequest.error: nil")
}
}
}
case .failure(let error):
Swift.print("fetchCKC error: \(error)")
DispatchQueue.main.async {
keyRequest.processContentKeyResponseError(error)
}
}
}
} catch {
Swift.print("SPC generation error: \(error)")
DispatchQueue.main.async {
keyRequest.processContentKeyResponseError(error)
}
}
}
}
private func fetchCKC(spcData: Data, assetId: String, completion: @escaping (Result) -> Void) {
if let contentKeyData = Data(base64Encoded: "X7sBORoVsqx/M96GnKLUqA==")
, let initialisationVectorData = Data(base64Encoded: "beXQ7IjxPAxSk29JU3MgvA==") {
let endpoint = "
if let url = URL(string: endpoint) {
let spcBase64 = spcData.base64EncodedString()
let contentKeyHex = ContentKeyDelegate.hex(forData: contentKeyData)
let initialisationVectorHex = ContentKeyDelegate.hex(forData: initialisationVectorData)
let parameters: [String: Any] = [
"fairplay-streaming-request": [
"version": 1,
"create-ckc": [[
"id": 1,
"asset-info": [[
"content-key": contentKeyHex,
"content-iv": initialisationVectorHex,
"encryption-scheme": "cbcs",
"hdcp-type": 0,
"content-type": "hd",
"lease-duration": 1200
]],
"spc": spcBase64
]]
]
]
do {
let postData = try JSONSerialization.data(withJSONObject: parameters)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = postData
let task = URLSession.shared.dataTask(with: request) {data, response, error in
if let response = response as? HTTPURLResponse {
Swift.print("KSM status code: \(response.statusCode)")
}
if let error = error {
Swift.print("KSM request error: \(error)")
completion(.failure(error))
return
}
guard let data = data, var responseString = String(data: data, encoding: .utf8) else {
Swift.print("KSM response empty or not UTF-8")
completion(.failure(ProgramError.noCKCReturnedByKSM))
return
}
if responseString.hasPrefix("Result") {
responseString.removeFirst("Result".count)
}
guard let jsonData = responseString.data(using: .utf8) else {
Swift.print("Failed to convert trimmed response to data")
completion(.failure(ProgramError.noCKCReturnedByKSM))
return
}
do {
if let dictionary = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any]
, let fairplayStreamingResponse = dictionary["fairplay-streaming-response"] as? [String: Any]
, let ckcArray = fairplayStreamingResponse["create-ckc"] as? [[String: Any]]
, let ckcDictionary = ckcArray.first
, let ckc = ckcDictionary["ckc"] as? String
, let thatData = Data(base64Encoded: ckc) {
completion(.success(thatData))
} else {
completion(.failure(ProgramError.noCKCReturnedByKSM))
}
} catch {
Swift.print("JSON parse error: \(error)")
completion(.failure(error))
}
}
task.resume()
} catch let error {
Swift.print(error)
completion(.failure(error))
}
} else {
let error = NSError()
completion(.failure(error))
}
}
}
enum ProgramError: Error {
case missingApplicationCertificate
case noCKCReturnedByKSM
}
func contentKeySession(_ session: AVContentKeySession, didFailWithError error: Error) {
Swift.print("AVContentKeySession didFailWithError: \(error.localizedDescription) — \(error)")
}
public static func hex(forData theData: Data) -> String {
return theData.map { String(format: "%02hhx", $0) }.joined()
}
}