From 89af43e4ffc19e3fc4403189a1013396ba856f51 Mon Sep 17 00:00:00 2001 From: marcin_wojnarowski Date: Thu, 27 Jan 2022 12:00:04 +0100 Subject: [PATCH 1/4] Format swift files --- ios/.swift-format | 7 + ios/Classes/ContentKeyManager.swift | 363 ++++++++++++++++------------ ios/Classes/DownloadItem.swift | 19 +- ios/Classes/DownloadManager.swift | 218 +++++++++-------- 4 files changed, 351 insertions(+), 256 deletions(-) create mode 100644 ios/.swift-format diff --git a/ios/.swift-format b/ios/.swift-format new file mode 100644 index 0000000..8bc040f --- /dev/null +++ b/ios/.swift-format @@ -0,0 +1,7 @@ +{ + "version": 1, + "lineLength": 100, + "indentation": { + "spaces": 4 + } +} diff --git a/ios/Classes/ContentKeyManager.swift b/ios/Classes/ContentKeyManager.swift index a013d14..508ba9b 100644 --- a/ios/Classes/ContentKeyManager.swift +++ b/ios/Classes/ContentKeyManager.swift @@ -1,82 +1,97 @@ @objc public class ContentKeyManager: NSObject, AVContentKeySessionDelegate { - + /// The singleton for `ContentKeyManager`. @objc public static let shared: ContentKeyManager = ContentKeyManager() - + /// A set containing the currently pending content key identifiers associated with persistable content key requests that have not been completed. var pendingPersistableContentKeyIdentifiers = Set() - - var certificatesMap = Dictionary() - var licensesMap = Dictionary() - var authHeadersMap = Dictionary() - + + var certificatesMap = [String: String]() + var licensesMap = [String: String]() + var authHeadersMap = [String: String]() + @objc public let contentKeySession: AVContentKeySession let contentKeyDelegateQueue = DispatchQueue(label: "ContentKeyDelegateQueue") - + /// The directory that is used to save persistable content keys. lazy var contentKeyDirectory: URL = { - guard let documentPath = - NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else { - fatalError("Unable to determine library URL") + guard + let documentPath = + NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first + else { + fatalError("Unable to determine library URL") } - + let documentURL = URL(fileURLWithPath: documentPath) - + let contentKeyDirectory = documentURL.appendingPathComponent(".keys", isDirectory: true) - + if !FileManager.default.fileExists(atPath: contentKeyDirectory.path, isDirectory: nil) { do { - try FileManager.default.createDirectory(at: contentKeyDirectory, - withIntermediateDirectories: false, - attributes: nil) + try FileManager.default.createDirectory( + at: contentKeyDirectory, + withIntermediateDirectories: false, + attributes: nil) } catch { - fatalError("Unable to create directory for content keys at path: \(contentKeyDirectory.path)") + fatalError( + "Unable to create directory for content keys at path: \(contentKeyDirectory.path)" + ) } } - + return contentKeyDirectory }() - + private override init() { - + contentKeySession = AVContentKeySession(keySystem: .fairPlayStreaming) super.init() contentKeySession.setDelegate(self, queue: contentKeyDelegateQueue) } - + @objc public func addRecipient(_ asset: AVURLAsset) { - contentKeySession.addContentKeyRecipient(asset); + contentKeySession.addContentKeyRecipient(asset) } - @objc public func addRecipient(_ asset: AVURLAsset, certificateUrl: String, licenseUrl: String, headers: Dictionary) { + @objc public func addRecipient( + _ asset: AVURLAsset, certificateUrl: String, licenseUrl: String, headers: [String: String] + ) { contentKeySession.addContentKeyRecipient(asset) certificatesMap.updateValue(certificateUrl, forKey: licenseUrl) - licensesMap.updateValue(licenseUrl.replacingOccurrences(of: "skd", with: "https"), forKey: licenseUrl) + licensesMap.updateValue( + licenseUrl.replacingOccurrences(of: "skd", with: "https"), forKey: licenseUrl) authHeadersMap.updateValue(headers["Authorization"] ?? "", forKey: licenseUrl) } - - public func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) { + + public func contentKeySession( + _ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest + ) { handleStreamingContentKeyRequest(keyRequest: keyRequest) } - - public func contentKeySession(_ session: AVContentKeySession, shouldRetry keyRequest: AVContentKeyRequest, - reason retryReason: AVContentKeyRequest.RetryReason) -> Bool { + + public func contentKeySession( + _ session: AVContentKeySession, shouldRetry keyRequest: AVContentKeyRequest, + reason retryReason: AVContentKeyRequest.RetryReason + ) -> Bool { return false } - + func handleStreamingContentKeyRequest(keyRequest: AVContentKeyRequest) { guard let contentKeyIdentifierString = keyRequest.identifier as? String, - let contentKeyIdentifierURL = URL(string: contentKeyIdentifierString), - let assetIDString = contentKeyIdentifierURL.host, - let assetIDData = assetIDString.data(using: .utf8), - let kid = URLComponents(string: contentKeyIdentifierString)?.queryItems?.first(where: {$0.name == "kid"})?.value + let contentKeyIdentifierURL = URL(string: contentKeyIdentifierString), + let assetIDString = contentKeyIdentifierURL.host, + let assetIDData = assetIDString.data(using: .utf8), + let kid = URLComponents(string: contentKeyIdentifierString)?.queryItems?.first(where: { + $0.name == "kid" + })?.value else { print("Failed to retrieve the assetID from the keyRequest!") return } let provideOnlinekey: () -> Void = { () -> Void in do { - let applicationCertificate = try self.requestApplicationCertificate(assetId: assetIDString, contentKeyIdentifier: contentKeyIdentifierString) + let applicationCertificate = try self.requestApplicationCertificate( + assetId: assetIDString, contentKeyIdentifier: contentKeyIdentifierString) let completionHandler = { [weak self] (spcData: Data?, error: Error?) in guard let strongSelf = self else { return } @@ -89,13 +104,16 @@ do { // Send SPC to Key Server and obtain CKC - let ckcData = try strongSelf.requestContentKeyFromKeySecurityModule(spcData: spcData, assetID: assetIDString, contentKeyIdentifier: contentKeyIdentifierString) + let ckcData = try strongSelf.requestContentKeyFromKeySecurityModule( + spcData: spcData, assetID: assetIDString, + contentKeyIdentifier: contentKeyIdentifierString) /* AVContentKeyResponse is used to represent the data returned from the key server when requesting a key for decrypting content. */ - let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: ckcData) + let keyResponse = AVContentKeyResponse( + fairPlayStreamingKeyResponseData: ckcData) /* Provide the content key response to make protected content available for processing. @@ -106,17 +124,19 @@ } } - keyRequest.makeStreamingContentKeyRequestData(forApp: applicationCertificate, - contentIdentifier: assetIDData, - options: [AVContentKeyRequestProtocolVersionsKey: [1]], - completionHandler: completionHandler) + keyRequest.makeStreamingContentKeyRequestData( + forApp: applicationCertificate, + contentIdentifier: assetIDData, + options: [AVContentKeyRequestProtocolVersionsKey: [1]], + completionHandler: completionHandler) } catch { keyRequest.processContentKeyResponseError(error) } } - - if (pendingPersistableContentKeyIdentifiers.contains(contentKeyIdentifierString) || - persistableContentKeyExistsOnDisk(withKid: kid)) { + + if pendingPersistableContentKeyIdentifiers.contains(contentKeyIdentifierString) + || persistableContentKeyExistsOnDisk(withKid: kid) + { // Request a Persistable Key Request. do { try keyRequest.respondByRequestingPersistableContentKeyRequestAndReturnError() @@ -132,8 +152,9 @@ provideOnlinekey() } } - - func requestApplicationCertificate(assetId: String, contentKeyIdentifier: String) throws -> Data { + + func requestApplicationCertificate(assetId: String, contentKeyIdentifier: String) throws -> Data + { // TODO: use proper urls var applicationCertificate: Data? = nil do { @@ -142,69 +163,85 @@ } catch { print("Error loading FairPlay application certificate: \(error)") } - + return applicationCertificate! } - - func requestContentKeyFromKeySecurityModule(spcData: Data, assetID: String, contentKeyIdentifier: String) throws -> Data { - + + func requestContentKeyFromKeySecurityModule( + spcData: Data, assetID: String, contentKeyIdentifier: String + ) throws -> Data { + var ckcData: Data? = nil - - let semaphore = DispatchSemaphore(value: 0) - let postString = "spc=\(spcData.base64EncodedString())&assetId=\(assetID)" - if let postData = postString.data(using: .ascii, allowLossyConversion: true), - let drmServerUrl = licensesMap.removeValue(forKey: contentKeyIdentifier) - { - var request = URLRequest(url: URL(string: drmServerUrl)!) - request.httpMethod = "POST" - request.setValue(String(postData.count), forHTTPHeaderField: "Content-Length") - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.setValue(authHeadersMap.removeValue(forKey: contentKeyIdentifier), forHTTPHeaderField: "Authorization") - request.httpBody = postData - - URLSession.shared.dataTask(with: request) { (data, _, error) in - if let data = data, var responseString = String(data: data, encoding: .utf8) { - responseString = responseString.replacingOccurrences(of: "", with: "").replacingOccurrences(of: "", with: "") - ckcData = Data(base64Encoded: responseString) - } else { - print("Error encountered while fetching FairPlay license: \(error?.localizedDescription ?? "Unknown error")") - } - - semaphore.signal() - }.resume() - } else { - fatalError("Invalid post data") - } - - semaphore.wait() - return ckcData! + + let semaphore = DispatchSemaphore(value: 0) + let postString = "spc=\(spcData.base64EncodedString())&assetId=\(assetID)" + if let postData = postString.data(using: .ascii, allowLossyConversion: true), + let drmServerUrl = licensesMap.removeValue(forKey: contentKeyIdentifier) + { + var request = URLRequest(url: URL(string: drmServerUrl)!) + request.httpMethod = "POST" + request.setValue(String(postData.count), forHTTPHeaderField: "Content-Length") + request.setValue( + "application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.setValue( + authHeadersMap.removeValue(forKey: contentKeyIdentifier), + forHTTPHeaderField: "Authorization") + request.httpBody = postData + + URLSession.shared.dataTask(with: request) { (data, _, error) in + if let data = data, var responseString = String(data: data, encoding: .utf8) { + responseString = responseString.replacingOccurrences(of: "", with: "") + .replacingOccurrences(of: "", with: "") + ckcData = Data(base64Encoded: responseString) + } else { + print( + "Error encountered while fetching FairPlay license: \(error?.localizedDescription ?? "Unknown error")" + ) + } + + semaphore.signal() + }.resume() + } else { + fatalError("Invalid post data") + } + + semaphore.wait() + return ckcData! } - + // MARK: PERSISTABLE - - public func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVPersistableContentKeyRequest) { + + public func contentKeySession( + _ session: AVContentKeySession, didProvide keyRequest: AVPersistableContentKeyRequest + ) { handlePersistableContentKeyRequest(keyRequest: keyRequest) } - - public func contentKeySession(_ session: AVContentKeySession, - didUpdatePersistableContentKey persistableContentKey: Data, - forContentKeyIdentifier keyIdentifier: Any) { - + + public func contentKeySession( + _ session: AVContentKeySession, + didUpdatePersistableContentKey persistableContentKey: Data, + forContentKeyIdentifier keyIdentifier: Any + ) { + guard let contentKeyIdentifierString = keyIdentifier as? String, - let kid = URLComponents(string: contentKeyIdentifierString)?.queryItems?.first(where: {$0.name == "kid"})?.value - else { - print("Failed to retrieve the assetID from the keyRequest!") - return + let kid = URLComponents(string: contentKeyIdentifierString)?.queryItems?.first(where: { + $0.name == "kid" + })?.value + else { + print("Failed to retrieve the assetID from the keyRequest!") + return } - + do { deletePeristableContentKey(withKid: kid) try writePersistableContentKey(contentKey: persistableContentKey, withKid: kid) } catch { - print("Failed to write updated persistable content key to disk: \(error.localizedDescription)") + print( + "Failed to write updated persistable content key to disk: \(error.localizedDescription)" + ) } } - + func handlePersistableContentKeyRequest(keyRequest: AVPersistableContentKeyRequest) { /* The key ID is the URI from the EXT-X-KEY tag in the playlist (e.g. "skd://key65") and the @@ -214,37 +251,44 @@ let contentKeyIdentifierURL = URL(string: contentKeyIdentifierString), let assetIDString = contentKeyIdentifierURL.host, let assetIDData = assetIDString.data(using: .utf8), - let kid = URLComponents(string: contentKeyIdentifierString)?.queryItems?.first(where: {$0.name == "kid"})?.value - else { - print("Failed to retrieve the assetID from the keyRequest!") - return + let kid = URLComponents(string: contentKeyIdentifierString)?.queryItems?.first(where: { + $0.name == "kid" + })?.value + else { + print("Failed to retrieve the assetID from the keyRequest!") + return } - + do { let provideOfflinekey: () -> Void = { () -> Void in if self.persistableContentKeyExistsOnDisk(withKid: kid) { - + let urlToPersistableKey = self.urlForPersistableContentKey(withKid: kid) - - guard let contentKey = FileManager.default.contents(atPath: urlToPersistableKey.path) else { + + guard + let contentKey = FileManager.default.contents( + atPath: urlToPersistableKey.path) + else { // Error Handling. - - self.pendingPersistableContentKeyIdentifiers.remove(contentKeyIdentifierString) - - keyRequest.processContentKeyResponseError(NSError()); + + self.pendingPersistableContentKeyIdentifiers.remove( + contentKeyIdentifierString) + + keyRequest.processContentKeyResponseError(NSError()) return } - + /* Create an AVContentKeyResponse from the persistent key data to use for requesting a key for decrypting content. */ - let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: contentKey) - + let keyResponse = AVContentKeyResponse( + fairPlayStreamingKeyResponseData: contentKey) + // Provide the content key response to make protected content available for processing. keyRequest.processContentKeyResponse(keyResponse) } else { - keyRequest.processContentKeyResponseError(NSError()); + keyRequest.processContentKeyResponseError(NSError()) } } @@ -252,85 +296,98 @@ guard let strongSelf = self else { return } if let error = error { keyRequest.processContentKeyResponseError(error) - - strongSelf.pendingPersistableContentKeyIdentifiers.remove(contentKeyIdentifierString) + + strongSelf.pendingPersistableContentKeyIdentifiers.remove( + contentKeyIdentifierString) return } - + guard let spcData = spcData else { return } - + do { // Send SPC to Key Server and obtain CKC - let ckcData = try strongSelf.requestContentKeyFromKeySecurityModule(spcData: spcData, assetID: assetIDString, contentKeyIdentifier: contentKeyIdentifierString) - - let persistentKey = try keyRequest.persistableContentKey(fromKeyVendorResponse: ckcData, options: nil) - - try strongSelf.writePersistableContentKey(contentKey: persistentKey, withKid: kid) - + let ckcData = try strongSelf.requestContentKeyFromKeySecurityModule( + spcData: spcData, assetID: assetIDString, + contentKeyIdentifier: contentKeyIdentifierString) + + let persistentKey = try keyRequest.persistableContentKey( + fromKeyVendorResponse: ckcData, options: nil) + + try strongSelf.writePersistableContentKey( + contentKey: persistentKey, withKid: kid) + /* AVContentKeyResponse is used to represent the data returned from the key server when requesting a key for decrypting content. */ - let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: persistentKey) - + let keyResponse = AVContentKeyResponse( + fairPlayStreamingKeyResponseData: persistentKey) + /* Provide the content key response to make protected content available for processing. */ keyRequest.processContentKeyResponse(keyResponse) - - NotificationCenter.default.post(name: .DidSavePersistableContentKey, - object: nil, - userInfo: ["url": contentKeyIdentifierURL]) - - strongSelf.pendingPersistableContentKeyIdentifiers.remove(contentKeyIdentifierString) + + NotificationCenter.default.post( + name: .DidSavePersistableContentKey, + object: nil, + userInfo: ["url": contentKeyIdentifierURL]) + + strongSelf.pendingPersistableContentKeyIdentifiers.remove( + contentKeyIdentifierString) } catch { provideOfflinekey() } } - if (certificatesMap[contentKeyIdentifierString] != nil) { - let applicationCertificate = try requestApplicationCertificate(assetId: assetIDString, contentKeyIdentifier: contentKeyIdentifierString) - - keyRequest.makeStreamingContentKeyRequestData(forApp: applicationCertificate, - contentIdentifier: assetIDData, - options: [AVContentKeyRequestProtocolVersionsKey: [1]], - completionHandler: completionHandler) + if certificatesMap[contentKeyIdentifierString] != nil { + let applicationCertificate = try requestApplicationCertificate( + assetId: assetIDString, contentKeyIdentifier: contentKeyIdentifierString) + + keyRequest.makeStreamingContentKeyRequestData( + forApp: applicationCertificate, + contentIdentifier: assetIDData, + options: [AVContentKeyRequestProtocolVersionsKey: [1]], + completionHandler: completionHandler) } else { provideOfflinekey() } } catch { - print("Failure responding to an AVPersistableContentKeyRequest when attemping to determine if key is already available for use on disk.") + print( + "Failure responding to an AVPersistableContentKeyRequest when attemping to determine if key is already available for use on disk." + ) } } - + @objc public func requestPersistableContentKeys(forUrl url: URL) { - + pendingPersistableContentKeyIdentifiers.insert(url.absoluteString) - - ContentKeyManager.shared.contentKeySession.processContentKeyRequest(withIdentifier: url.absoluteString, initializationData: nil, options: nil) + + ContentKeyManager.shared.contentKeySession.processContentKeyRequest( + withIdentifier: url.absoluteString, initializationData: nil, options: nil) } - + func writePersistableContentKey(contentKey: Data, withKid kid: String) throws { let fileURL = urlForPersistableContentKey(withKid: kid) - + try contentKey.write(to: fileURL, options: Data.WritingOptions.atomicWrite) } - + func urlForPersistableContentKey(withKid kid: String) -> URL { return contentKeyDirectory.appendingPathComponent("\(kid)-Key") } - + func persistableContentKeyExistsOnDisk(withKid kid: String) -> Bool { let contentKeyURL = urlForPersistableContentKey(withKid: kid) - + return FileManager.default.fileExists(atPath: contentKeyURL.path) } - + @objc public func deletePeristableContentKey(withKid kid: String) { - + guard persistableContentKeyExistsOnDisk(withKid: kid) else { return } - + let contentKeyURL = urlForPersistableContentKey(withKid: kid) - + do { try FileManager.default.removeItem(at: contentKeyURL) UserDefaults.standard.removeObject(forKey: "\(kid)-Key") @@ -341,10 +398,10 @@ } extension Notification.Name { - + /** The notification that is posted when the content key for a given asset has been saved to disk. */ - static let DidSavePersistableContentKey = Notification.Name("ContentKeyDelegateDidSavePersistableContentKey") + static let DidSavePersistableContentKey = Notification.Name( + "ContentKeyDelegateDidSavePersistableContentKey") } - diff --git a/ios/Classes/DownloadItem.swift b/ios/Classes/DownloadItem.swift index 2de7e7c..559ec41 100644 --- a/ios/Classes/DownloadItem.swift +++ b/ios/Classes/DownloadItem.swift @@ -1,23 +1,26 @@ public class DownloadItem: NSObject, FlutterStreamHandler { - public var eventSink: FlutterEventSink? = nil - + public let urlAsset: AVURLAsset - + public let downloadData: String - + public var localUrl: URL? = nil - + init(urlAsset: AVURLAsset, downloadData: String) { self.urlAsset = urlAsset self.downloadData = downloadData } - - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + + public func onListen( + withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink + ) + -> FlutterError? + { eventSink = events return nil } - + public func onCancel(withArguments arguments: Any?) -> FlutterError? { eventSink = nil return nil diff --git a/ios/Classes/DownloadManager.swift b/ios/Classes/DownloadManager.swift index 151b9a1..a5ec614 100644 --- a/ios/Classes/DownloadManager.swift +++ b/ios/Classes/DownloadManager.swift @@ -1,47 +1,58 @@ -import Foundation import AVFoundation +import Foundation @objc public class DownloadManager: NSObject { - /// Singleton for DownloadManager. @objc public static let sharedManager = DownloadManager() - + /// The AVAssetDownloadURLSession to use for managing AVAssetDownloadTasks. fileprivate var assetDownloadURLSession: AVAssetDownloadURLSession! - + /// Internal map of AVAggregateAssetDownloadTask to its corresponding DownloadItem. fileprivate var activeDownloadsMap = [AVAggregateAssetDownloadTask: DownloadItem]() - + fileprivate var pendingAssetsMaps = [String: AVURLAsset]() - + fileprivate var pendingDataStringMap = [String: String]() - + fileprivate var pendingEventChannelMap = [String: FlutterEventChannel]() - + fileprivate var pendingFlutterResultMap = [String: FlutterResult]() - + fileprivate let dataPrefix = "Data" - + fileprivate let bookmarkPrefix = "Bookmark" - + override private init() { - super.init() - - let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "betterplayer-download") - + + let backgroundConfiguration = URLSessionConfiguration.background( + withIdentifier: "betterplayer-download") + assetDownloadURLSession = - AVAssetDownloadURLSession(configuration: backgroundConfiguration, - assetDownloadDelegate: self, delegateQueue: OperationQueue.main) - - NotificationCenter.default.addObserver(self, selector: #selector(handleContentKeyDelegateDidSavePersistableContentKey(notification:)), name: .DidSavePersistableContentKey, object: nil) - + AVAssetDownloadURLSession( + configuration: backgroundConfiguration, + assetDownloadDelegate: self, + delegateQueue: OperationQueue.main) + + NotificationCenter.default.addObserver( + self, + selector: #selector( + handleContentKeyDelegateDidSavePersistableContentKey(notification:)), + name: .DidSavePersistableContentKey, object: nil) } - - @objc public func download(_ url: URL, dataString: String, licenseUrl: URL?, certificateUrl: URL?, drmHeaders: Dictionary, eventChannel: FlutterEventChannel, result: @escaping FlutterResult) { - - if (activeDownloadsMap.keys.contains { key in // TODO: check if in pending - if (key.urlAsset.url == url) { + + @objc public func download( + _ url: URL, + dataString: String, + licenseUrl: URL?, + certificateUrl: URL?, + drmHeaders: [String: String], + eventChannel: FlutterEventChannel, + result: @escaping FlutterResult + ) { + if (activeDownloadsMap.keys.contains { key in // TODO: check if in pending + if key.urlAsset.url == url { return true } else { return false @@ -51,82 +62,97 @@ import AVFoundation result(nil) return } - + let urlAsset = AVURLAsset(url: url) - - if (licenseUrl != nil && certificateUrl != nil) { + + if let licenseUrl = licenseUrl, let certificateUrl = certificateUrl { var kidMap = UserDefaults.standard.dictionary(forKey: "kid_map") ?? [String: String]() - let kid = URLComponents(url: licenseUrl!, resolvingAgainstBaseURL: false)?.queryItems?.first(where: {$0.name == "kid"})?.value + let kid = URLComponents(url: licenseUrl, resolvingAgainstBaseURL: false)?.queryItems? + .first(where: { $0.name == "kid" })?.value kidMap.updateValue(kid, forKey: url.absoluteString) UserDefaults.standard.setValue(kidMap, forKey: "kid_map") - - ContentKeyManager.shared.addRecipient(urlAsset, certificateUrl: certificateUrl!.absoluteString, licenseUrl: licenseUrl!.absoluteString, headers: drmHeaders) - ContentKeyManager.shared.requestPersistableContentKeys(forUrl: licenseUrl!) - pendingAssetsMaps[licenseUrl!.absoluteString] = urlAsset - pendingDataStringMap[licenseUrl!.absoluteString] = dataString - pendingEventChannelMap[licenseUrl!.absoluteString] = eventChannel - pendingFlutterResultMap[licenseUrl!.absoluteString] = result + + ContentKeyManager.shared.addRecipient( + urlAsset, certificateUrl: certificateUrl.absoluteString, + licenseUrl: licenseUrl.absoluteString, headers: drmHeaders) + ContentKeyManager.shared.requestPersistableContentKeys(forUrl: licenseUrl) + pendingAssetsMaps[licenseUrl.absoluteString] = urlAsset + pendingDataStringMap[licenseUrl.absoluteString] = dataString + pendingEventChannelMap[licenseUrl.absoluteString] = eventChannel + pendingFlutterResultMap[licenseUrl.absoluteString] = result download(urlAsset, dataString: dataString, eventChannel: eventChannel, result: result) } else { download(urlAsset, dataString: dataString, eventChannel: eventChannel, result: result) } } - - private func download(_ urlAsset: AVURLAsset, dataString: String, eventChannel: FlutterEventChannel, result: FlutterResult){ - guard let task = - assetDownloadURLSession.aggregateAssetDownloadTask(with: urlAsset, - mediaSelections: [urlAsset.preferredMediaSelection], - assetTitle: "", - assetArtworkData: nil, - options: - [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 265_000]) else { return } - + + private func download( + _ urlAsset: AVURLAsset, dataString: String, eventChannel: FlutterEventChannel, + result: FlutterResult + ) { + guard + let task = + assetDownloadURLSession.aggregateAssetDownloadTask( + with: urlAsset, + mediaSelections: [urlAsset.preferredMediaSelection], + assetTitle: "", + assetArtworkData: nil, + options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: 265_000]) + else { return } + let downloadItem = DownloadItem(urlAsset: urlAsset, downloadData: dataString) eventChannel.setStreamHandler(downloadItem) activeDownloadsMap[task] = downloadItem - + task.resume() result(nil) } - + /// Returns an AVURLAsset pointing to a file on disk if it exists. @objc public func localAsset(url: URL) -> AVURLAsset? { let userDefaults = UserDefaults.standard - guard let localFileLocation = userDefaults.value(forKey: bookmarkPrefix + url.absoluteString) as? Data else { return nil } - + guard + let localFileLocation = userDefaults.value(forKey: bookmarkPrefix + url.absoluteString) + as? Data + else { return nil } + var bookmarkDataIsStale = false do { - let localUrl = try URL(resolvingBookmarkData: localFileLocation, - bookmarkDataIsStale: &bookmarkDataIsStale) - + let localUrl = try URL( + resolvingBookmarkData: localFileLocation, + bookmarkDataIsStale: &bookmarkDataIsStale) + if bookmarkDataIsStale { userDefaults.removeObject(forKey: bookmarkPrefix + url.absoluteString) userDefaults.removeObject(forKey: dataPrefix + url.absoluteString) fatalError("Bookmark data is stale!") } - + let urlAsset = AVURLAsset(url: localUrl) - + return urlAsset } catch { fatalError("Failed to create URL from bookmark with error: \(error)") } } - - @objc public func downloadedAssets() -> Dictionary { + + @objc public func downloadedAssets() -> [String: String] { let userDefaults = UserDefaults.standard - let downloads = userDefaults.dictionaryRepresentation().filter { (key, _) -> Bool in - key.hasPrefix(dataPrefix) - } as! [String: String] - - return Dictionary(uniqueKeysWithValues: downloads.map - { key, value in (String(key.suffix(from: key.index(key.startIndex, offsetBy: 4))), value) }) + let downloads = + userDefaults.dictionaryRepresentation().filter { key, _ -> Bool in + key.hasPrefix(dataPrefix) + } as! [String: String] + + return [String: String]( + uniqueKeysWithValues: downloads.map { key, value in + (String(key.suffix(from: key.index(key.startIndex, offsetBy: 4))), value) + }) } - + /// Deletes an Asset on disk if possible. @objc public func deleteAsset(_ url: URL) { let userDefaults = UserDefaults.standard - + do { if let localFileLocation = localAsset(url: url)?.url { try FileManager.default.removeItem(at: localFileLocation) @@ -137,41 +163,41 @@ import AVFoundation userDefaults.removeObject(forKey: bookmarkPrefix + url.absoluteString) userDefaults.removeObject(forKey: dataPrefix + url.absoluteString) } - + @objc func handleContentKeyDelegateDidSavePersistableContentKey(notification: Notification) { do { let url = notification.userInfo?["url"] as! URL - let urlAsset = self.pendingAssetsMaps.removeValue(forKey: url.absoluteString)! - let dataString = self.pendingDataStringMap.removeValue(forKey: url.absoluteString)! - let eventChannel = self.pendingEventChannelMap.removeValue(forKey: url.absoluteString)! - let result = self.pendingFlutterResultMap.removeValue(forKey: url.absoluteString)! - + let urlAsset = pendingAssetsMaps.removeValue(forKey: url.absoluteString)! + let dataString = pendingDataStringMap.removeValue(forKey: url.absoluteString)! + let eventChannel = pendingEventChannelMap.removeValue(forKey: url.absoluteString)! + let result = pendingFlutterResultMap.removeValue(forKey: url.absoluteString)! + download(urlAsset, dataString: dataString, eventChannel: eventChannel, result: result) - } catch{ + } catch { print("error while retrieving pending download values") } } - } extension DownloadManager: AVAssetDownloadDelegate { - /// Tells the delegate that the task finished transferring data. - public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - - let downloadItem = activeDownloadsMap.removeValue(forKey: task as! AVAggregateAssetDownloadTask) - - if (error == nil) { + public func urlSession( + _ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error? + ) { + let downloadItem = activeDownloadsMap.removeValue( + forKey: task as! AVAggregateAssetDownloadTask) + + if error == nil { downloadItem?.eventSink?(100.0) } downloadItem?.eventSink?(FlutterEndOfEventStream) - + let userDefaults = UserDefaults.standard let localURL = downloadItem?.localUrl - + if let error = error as NSError? { - if (error.code == NSURLErrorCancelled) { + if error.code == NSURLErrorCancelled { do { try FileManager.default.removeItem(at: localURL!) if let urlString = downloadItem?.urlAsset.url.absoluteString { @@ -186,11 +212,11 @@ extension DownloadManager: AVAssetDownloadDelegate { do { let bookmark = try localURL?.bookmarkData() let urlString = (downloadItem?.urlAsset.url.absoluteString) - - if(!(bookmark==nil || urlString==nil)){ + + if !(bookmark == nil || urlString == nil) { userDefaults.set(bookmark, forKey: bookmarkPrefix + urlString!) userDefaults.set(downloadItem?.downloadData, forKey: dataPrefix + urlString!) - }else { + } else { print("Failed to create bookmarkData for download URL.") } } catch { @@ -198,26 +224,28 @@ extension DownloadManager: AVAssetDownloadDelegate { } } } - + /// Method called when the an aggregate download task determines the location this asset will be downloaded to. - public func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, - willDownloadTo location: URL) { - + public func urlSession( + _ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, + willDownloadTo location: URL + ) { activeDownloadsMap[aggregateAssetDownloadTask]?.localUrl = location } - + /// Method to adopt to subscribe to progress updates of an AVAggregateAssetDownloadTask. - public func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, - didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], - timeRangeExpectedToLoad: CMTimeRange, for mediaSelection: AVMediaSelection) { - + public func urlSession( + _ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, + didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], + timeRangeExpectedToLoad: CMTimeRange, for mediaSelection: AVMediaSelection + ) { var percentComplete = 0.0 for value in loadedTimeRanges { let loadedTimeRange: CMTimeRange = value.timeRangeValue percentComplete += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds } - + if let streamHandler = activeDownloadsMap[aggregateAssetDownloadTask] { streamHandler.eventSink?(percentComplete * 100) } From b499d37857efe97578ac92533d4a4f61d87b7d5f Mon Sep 17 00:00:00 2001 From: marcin_wojnarowski Date: Thu, 27 Jan 2022 19:09:16 +0100 Subject: [PATCH 2/4] Refactor and remove duplicate download call --- ios/Classes/BetterPlayerPlugin.m | 1 + ios/Classes/ContentKeyManager.swift | 10 ++- ios/Classes/DownloadManager.swift | 103 +++++++++++++++------------- pubspec.lock | 14 ++-- 4 files changed, 71 insertions(+), 57 deletions(-) diff --git a/ios/Classes/BetterPlayerPlugin.m b/ios/Classes/BetterPlayerPlugin.m index cc42507..61646df 100644 --- a/ios/Classes/BetterPlayerPlugin.m +++ b/ios/Classes/BetterPlayerPlugin.m @@ -486,6 +486,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call } else if ([@"removeAsset" isEqualToString:call.method]) { NSString *url = argsMap[@"url"]; + // TODO: this handling is wrong, we should first check whether downloading is happening already NSDictionary *kidMap = [NSUserDefaults.standardUserDefaults dictionaryForKey:@"kid_map"]; NSString *kid = [kidMap valueForKey:url]; diff --git a/ios/Classes/ContentKeyManager.swift b/ios/Classes/ContentKeyManager.swift index 508ba9b..cd646ac 100644 --- a/ios/Classes/ContentKeyManager.swift +++ b/ios/Classes/ContentKeyManager.swift @@ -274,7 +274,10 @@ self.pendingPersistableContentKeyIdentifiers.remove( contentKeyIdentifierString) - keyRequest.processContentKeyResponseError(NSError()) + keyRequest.processContentKeyResponseError( + NSError.init( + domain: "handlePersistableContentKeyRequest", code: 1, userInfo: nil + )) return } @@ -288,7 +291,10 @@ // Provide the content key response to make protected content available for processing. keyRequest.processContentKeyResponse(keyResponse) } else { - keyRequest.processContentKeyResponseError(NSError()) + keyRequest.processContentKeyResponseError( + NSError.init( + domain: "handlePersistableContentKeyRequest", code: 2, userInfo: nil + )) } } diff --git a/ios/Classes/DownloadManager.swift b/ios/Classes/DownloadManager.swift index a5ec614..4b01b22 100644 --- a/ios/Classes/DownloadManager.swift +++ b/ios/Classes/DownloadManager.swift @@ -11,13 +11,7 @@ import Foundation /// Internal map of AVAggregateAssetDownloadTask to its corresponding DownloadItem. fileprivate var activeDownloadsMap = [AVAggregateAssetDownloadTask: DownloadItem]() - fileprivate var pendingAssetsMaps = [String: AVURLAsset]() - - fileprivate var pendingDataStringMap = [String: String]() - - fileprivate var pendingEventChannelMap = [String: FlutterEventChannel]() - - fileprivate var pendingFlutterResultMap = [String: FlutterResult]() + fileprivate var pendingKeyDataMap = [String: PendingKeyData]() fileprivate let dataPrefix = "Data" @@ -51,14 +45,11 @@ import Foundation eventChannel: FlutterEventChannel, result: @escaping FlutterResult ) { - if (activeDownloadsMap.keys.contains { key in // TODO: check if in pending - if key.urlAsset.url == url { - return true - } else { - return false - } - }) { - // download for this url is already in progress + let isInProgress = + activeDownloadsMap.keys.contains { $0.urlAsset.url == url } + || pendingKeyDataMap.values.contains { $0.asset.url == url } + + if isInProgress { result(nil) return } @@ -69,25 +60,31 @@ import Foundation var kidMap = UserDefaults.standard.dictionary(forKey: "kid_map") ?? [String: String]() let kid = URLComponents(url: licenseUrl, resolvingAgainstBaseURL: false)?.queryItems? .first(where: { $0.name == "kid" })?.value - kidMap.updateValue(kid, forKey: url.absoluteString) - UserDefaults.standard.setValue(kidMap, forKey: "kid_map") + if let kid = kid { + kidMap.updateValue(kid, forKey: url.absoluteString) + UserDefaults.standard.setValue(kidMap, forKey: "kid_map") + } else { + print("Failed to find kid in licenseUrl: \(licenseUrl)") + return + } + + pendingKeyDataMap[licenseUrl.absoluteString] = PendingKeyData( + asset: urlAsset, dataString: dataString, + eventChannel: eventChannel, flutterResult: result) ContentKeyManager.shared.addRecipient( urlAsset, certificateUrl: certificateUrl.absoluteString, licenseUrl: licenseUrl.absoluteString, headers: drmHeaders) ContentKeyManager.shared.requestPersistableContentKeys(forUrl: licenseUrl) - pendingAssetsMaps[licenseUrl.absoluteString] = urlAsset - pendingDataStringMap[licenseUrl.absoluteString] = dataString - pendingEventChannelMap[licenseUrl.absoluteString] = eventChannel - pendingFlutterResultMap[licenseUrl.absoluteString] = result - download(urlAsset, dataString: dataString, eventChannel: eventChannel, result: result) } else { download(urlAsset, dataString: dataString, eventChannel: eventChannel, result: result) } } private func download( - _ urlAsset: AVURLAsset, dataString: String, eventChannel: FlutterEventChannel, + _ urlAsset: AVURLAsset, + dataString: String, + eventChannel: FlutterEventChannel, result: FlutterResult ) { guard @@ -139,9 +136,8 @@ import Foundation @objc public func downloadedAssets() -> [String: String] { let userDefaults = UserDefaults.standard let downloads = - userDefaults.dictionaryRepresentation().filter { key, _ -> Bool in - key.hasPrefix(dataPrefix) - } as! [String: String] + userDefaults.dictionaryRepresentation().filter { $0.key.hasPrefix(dataPrefix) } + as! [String: String] return [String: String]( uniqueKeysWithValues: downloads.map { key, value in @@ -166,17 +162,19 @@ import Foundation @objc func handleContentKeyDelegateDidSavePersistableContentKey(notification: Notification) { - do { - let url = notification.userInfo?["url"] as! URL - let urlAsset = pendingAssetsMaps.removeValue(forKey: url.absoluteString)! - let dataString = pendingDataStringMap.removeValue(forKey: url.absoluteString)! - let eventChannel = pendingEventChannelMap.removeValue(forKey: url.absoluteString)! - let result = pendingFlutterResultMap.removeValue(forKey: url.absoluteString)! - - download(urlAsset, dataString: dataString, eventChannel: eventChannel, result: result) - } catch { + guard + let url = notification.userInfo?["url"] as? URL, + let pendingKeyData = pendingKeyDataMap.removeValue(forKey: url.absoluteString) + else { print("error while retrieving pending download values") + return } + + download( + pendingKeyData.asset, + dataString: pendingKeyData.dataString, + eventChannel: pendingKeyData.eventChannel, + result: pendingKeyData.flutterResult) } } @@ -196,26 +194,27 @@ extension DownloadManager: AVAssetDownloadDelegate { let userDefaults = UserDefaults.standard let localURL = downloadItem?.localUrl - if let error = error as NSError? { - if error.code == NSURLErrorCancelled { + if error != nil { + if let localURL = localURL { do { - try FileManager.default.removeItem(at: localURL!) - if let urlString = downloadItem?.urlAsset.url.absoluteString { - userDefaults.removeObject(forKey: bookmarkPrefix + urlString) - userDefaults.removeObject(forKey: dataPrefix + urlString) - } + try FileManager.default.removeItem(at: localURL) } catch { print("An error occured trying to delete the contents on disk for: \(error)") } } + + if let urlString = downloadItem?.urlAsset.url.absoluteString { + userDefaults.removeObject(forKey: bookmarkPrefix + urlString) + userDefaults.removeObject(forKey: dataPrefix + urlString) + } } else { do { let bookmark = try localURL?.bookmarkData() - let urlString = (downloadItem?.urlAsset.url.absoluteString) + let urlString = downloadItem?.urlAsset.url.absoluteString - if !(bookmark == nil || urlString == nil) { - userDefaults.set(bookmark, forKey: bookmarkPrefix + urlString!) - userDefaults.set(downloadItem?.downloadData, forKey: dataPrefix + urlString!) + if let bookmark = bookmark, let urlString = urlString { + userDefaults.set(bookmark, forKey: bookmarkPrefix + urlString) + userDefaults.set(downloadItem?.downloadData, forKey: dataPrefix + urlString) } else { print("Failed to create bookmarkData for download URL.") } @@ -227,7 +226,8 @@ extension DownloadManager: AVAssetDownloadDelegate { /// Method called when the an aggregate download task determines the location this asset will be downloaded to. public func urlSession( - _ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, + _ session: URLSession, + aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, willDownloadTo location: URL ) { activeDownloadsMap[aggregateAssetDownloadTask]?.localUrl = location @@ -241,7 +241,7 @@ extension DownloadManager: AVAssetDownloadDelegate { ) { var percentComplete = 0.0 for value in loadedTimeRanges { - let loadedTimeRange: CMTimeRange = value.timeRangeValue + let loadedTimeRange = value.timeRangeValue percentComplete += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds } @@ -251,3 +251,10 @@ extension DownloadManager: AVAssetDownloadDelegate { } } } + +private struct PendingKeyData { + public let asset: AVURLAsset + public let dataString: String + public let eventChannel: FlutterEventChannel + public let flutterResult: FlutterResult +} diff --git a/pubspec.lock b/pubspec.lock index 2be5ac9..1dd4027 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.7.0" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -139,14 +139,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" meta: dependency: "direct main" description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.7.0" path: dependency: transitive description: @@ -270,7 +270,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" + version: "0.4.3" typed_data: dependency: transitive description: @@ -284,7 +284,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" visibility_detector: dependency: "direct main" description: @@ -349,5 +349,5 @@ packages: source: hosted version: "5.1.2" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.14.0 <3.0.0" flutter: ">=2.0.0" From 0b16f6cd013bb94d6faad51ec7ff7559c9c1e941 Mon Sep 17 00:00:00 2001 From: marcin_wojnarowski Date: Fri, 28 Jan 2022 10:14:02 +0100 Subject: [PATCH 3/4] Fix force unwrap --- ios/Classes/ContentKeyManager.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ios/Classes/ContentKeyManager.swift b/ios/Classes/ContentKeyManager.swift index cd646ac..eed9f9c 100644 --- a/ios/Classes/ContentKeyManager.swift +++ b/ios/Classes/ContentKeyManager.swift @@ -172,6 +172,7 @@ ) throws -> Data { var ckcData: Data? = nil + var ckcError: Error? = nil let semaphore = DispatchSemaphore(value: 0) let postString = "spc=\(spcData.base64EncodedString())&assetId=\(assetID)" @@ -197,6 +198,7 @@ print( "Error encountered while fetching FairPlay license: \(error?.localizedDescription ?? "Unknown error")" ) + ckcError = error } semaphore.signal() @@ -206,7 +208,12 @@ } semaphore.wait() - return ckcData! + + if let ckcData = ckcData { + return ckcData + } else { + throw ckcError! + } } // MARK: PERSISTABLE From 76ee38a3886922700216dc41d18c2b871c23511d Mon Sep 17 00:00:00 2001 From: marcin_wojnarowski Date: Fri, 28 Jan 2022 19:01:05 +0100 Subject: [PATCH 4/4] Move download to main thread --- ios/Classes/DownloadManager.swift | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/ios/Classes/DownloadManager.swift b/ios/Classes/DownloadManager.swift index 4b01b22..ed03ee3 100644 --- a/ios/Classes/DownloadManager.swift +++ b/ios/Classes/DownloadManager.swift @@ -162,19 +162,24 @@ import Foundation @objc func handleContentKeyDelegateDidSavePersistableContentKey(notification: Notification) { - guard - let url = notification.userInfo?["url"] as? URL, - let pendingKeyData = pendingKeyDataMap.removeValue(forKey: url.absoluteString) - else { - print("error while retrieving pending download values") - return - } + // This handle can be called from different threads. NotificationCenter runs callbacks in the thread that posted the notification. + // Thus we move excecution to the main thread to avoid data races. + + DispatchQueue.main.async { + guard + let url = notification.userInfo?["url"] as? URL, + let pendingKeyData = self.pendingKeyDataMap.removeValue(forKey: url.absoluteString) + else { + print("error while retrieving pending download values") + return + } - download( - pendingKeyData.asset, - dataString: pendingKeyData.dataString, - eventChannel: pendingKeyData.eventChannel, - result: pendingKeyData.flutterResult) + self.download( + pendingKeyData.asset, + dataString: pendingKeyData.dataString, + eventChannel: pendingKeyData.eventChannel, + result: pendingKeyData.flutterResult) + } } }