Skip to content

Commit

Permalink
Merge branch 'push-drm-mocking' into maint/drm-unit-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
daytime-em committed May 7, 2024
2 parents d786452 + 18877d0 commit c21421d
Show file tree
Hide file tree
Showing 11 changed files with 477 additions and 298 deletions.
37 changes: 28 additions & 9 deletions Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,24 @@
import Foundation
import AVFoundation

class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {

class ContentKeySessionDelegate<SessionManager: FairPlayStreamingSessionManager> : NSObject, AVContentKeySessionDelegate {

weak var sessionManager: SessionManager?

init(
sessionManager: SessionManager
) {
self.sessionManager = sessionManager
}

// MARK: AVContentKeySessionDelegate implementation

func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) {
handleContentKeyRequest(session, request: keyRequest)
handleContentKeyRequest(request: keyRequest)
}

func contentKeySession(_ session: AVContentKeySession, didProvideRenewingContentKeyRequest keyRequest: AVContentKeyRequest) {
handleContentKeyRequest(session, request: keyRequest)
handleContentKeyRequest(request: keyRequest)
}

func contentKeySession(_ session: AVContentKeySession, contentKeyRequestDidSucceed keyRequest: AVContentKeyRequest) {
Expand Down Expand Up @@ -95,7 +103,7 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
return playbackID
}

func handleContentKeyRequest(_ session: AVContentKeySession, request: AVContentKeyRequest) {
func handleContentKeyRequest(request: AVContentKeyRequest) {
print("<><>handleContentKeyRequest: Called")
// for hls, "the identifier must be an NSURL that matches a key URI in the Media Playlist." from the docs
guard let keyURLStr = request.identifier as? String,
Expand All @@ -112,8 +120,14 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
return
}

let playbackOptions = PlayerSDK.shared.fairPlaySessionManager
.findRegisteredPlaybackOptions(for: playbackID)
guard let sessionManager = self.sessionManager else {
print("Missing Session Manager")
return
}

let playbackOptions = sessionManager.findRegisteredPlaybackOptions(
for: playbackID
)
guard let playbackOptions = playbackOptions,
case .drm(let drmOptions) = playbackOptions.playbackPolicy else {
print("DRM Tokens must be registered when the AVPlayerItem is created, using FairplaySessionManager")
Expand All @@ -127,7 +141,7 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
// the drmtoday example does this by joining a dispatch group, but is this best?
let group = DispatchGroup()
group.enter()
PlayerSDK.shared.fairPlaySessionManager.requestCertificate(
sessionManager.requestCertificate(
fromDomain: rootDomain,
playbackID: playbackID,
drmToken: drmOptions.drmToken,
Expand Down Expand Up @@ -178,11 +192,16 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
rootDomain: String, // without any "license." or "stream." prepended, eg mux.com, custom.1234.co.uk
request: AVContentKeyRequest
) {
guard let sessionManager = self.sessionManager else {
print("Missing Session Manager")
return
}

// todo - DRM Today example does this by joining a DispatchGroup. Is this really preferable??
var ckcData: Data? = nil
let group = DispatchGroup()
group.enter()
PlayerSDK.shared.fairPlaySessionManager.requestLicense(
sessionManager.requestLicense(
spcData: spcData,
playbackID: playbackID,
drmToken: drmToken,
Expand Down
203 changes: 123 additions & 80 deletions Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@
import Foundation
import AVFoundation

protocol FairPlaySessionManager {

// MARK: - FairPlayStreamingSessionManager

// Use AnyObject to restrict conformances only to reference
// types because the SDKs AVContentKeySessionDelegate holds
// a weak reference to the SDKs witness of this.
protocol FairPlayStreamingSessionCredentialClient: AnyObject {
// MARK: Requesting licenses and certs
/// Requests the App Certificate for a playback id

// Requests the App Certificate for a playback id
func requestCertificate(
fromDomain rootDomain: String,
playbackID: String,
drmToken: String,
completion requestCompletion: @escaping (Result<Data, Error>) -> Void
)
/// Requests a license to play based on the given SPC data
/// - parameter offline - Not currently used, may not ever be used in short-term, maybe delete?
// Requests a license to play based on the given SPC data
// - parameter offline - Not currently used, may not ever be used in short-term, maybe delete?
func requestLicense(
spcData: Data,
playbackID: String,
Expand All @@ -29,14 +33,11 @@ protocol FairPlaySessionManager {
offline _: Bool,
completion requestCompletion: @escaping (Result<Data, Error>) -> Void
)

// MARK: registering drm-protected assets

/// Adds a ``AVContentKeyRecipient`` (probably an ``AVURLAsset``) that must be played
/// with DRM protection. This call is necessary for DRM playback to succeed
func addContentKeyRecipient(_ recipient: AVContentKeyRecipient)
/// Removes a ``AVContentKeyRecipient`` previously added by ``addContentKeyRecipient``
func removeContentKeyRecipient(_ recipient: AVContentKeyRecipient)
}

// MARK: - PlaybackOptionsRegistry

protocol PlaybackOptionsRegistry {
/// Registers a ``PlaybackOptions`` for DRM playback, associated with the given playbackID
func registerPlaybackOptions(_ opts: PlaybackOptions, for playbackID: String)
/// Gets a DRM token previously registered via ``registerPlaybackOptions``
Expand All @@ -45,58 +46,113 @@ protocol FairPlaySessionManager {
func unregisterPlaybackOptions(for playbackID: String)
}

// MARK: - ContentKeyRecipientRegistry

// Intended for registering drm-protected AVURLAssets
protocol ContentKeyRecipientRegistry {
/// Adds a ``AVContentKeyRecipient`` (probably an ``AVURLAsset``) that must be played
/// with DRM protection. This call is necessary for DRM playback to succeed
func addContentKeyRecipient(_ recipient: AVContentKeyRecipient)
/// Removes a ``AVContentKeyRecipient`` previously added by ``addContentKeyRecipient``
func removeContentKeyRecipient(_ recipient: AVContentKeyRecipient)
}

// MARK: - FairPlayStreamingSessionManager

typealias FairPlayStreamingSessionManager = FairPlayStreamingSessionCredentialClient & PlaybackOptionsRegistry & ContentKeyRecipientRegistry

// MARK: - Content Key Provider

// Define protocol for calls made to AVContentKeySession
protocol ContentKeyProvider {
func setDelegate(
_ delegate: (any AVContentKeySessionDelegate)?,
queue delegateQueue: dispatch_queue_t?
)

func addContentKeyRecipient(_ recipient: any AVContentKeyRecipient)

func removeContentKeyRecipient(_ recipient: any AVContentKeyRecipient)
}

// AVContentKeySession already has built-in definitions for
// these methods so this declaration can be empty
extension AVContentKeySession: ContentKeyProvider { }

// MARK: helpers for interacting with the license server

extension DefaultFPSSManager {
/// Generates a domain name appropriate for the Mux license proxy associted with the given
/// "root domain". For example `mux.com` returns `license.mux.com` and
/// `customdomain.xyz.com` returns `license.customdomain.xyz.com`
static func makeLicenseDomain(_ rootDomain: String) -> String {
extension String {
// Generates a domain name appropriate for the Mux license proxy associted with the given
// "root domain". For example `mux.com` returns `license.mux.com` and
// `customdomain.xyz.com` returns `license.customdomain.xyz.com`
static func makeLicenseDomain(rootDomain: String) -> Self {
let customDomainWithDefault = rootDomain
let licenseDomain = "license.\(customDomainWithDefault)"

// TODO: this check should not reach production or playing from staging will probably break
if("staging.mux.com" == customDomainWithDefault) {
return "license.gcp-us-west1-vos1.staging.mux.com"
} else {
return licenseDomain
}
}

/// Generates an authenticated URL to Mux's license proxy, for a 'license' (a CKC for fairplay),
/// for the given playabckID and DRM Token, at the given domain
/// - SeeAlso ``makeLicenseDomain``
static func makeLicenseURL(playbackID: String, drmToken: String, licenseDomain: String) -> URL {
let baseStr = "https://\(licenseDomain)/license/fairplay/\(playbackID)?token=\(drmToken)"
let url = URL(string: baseStr)
return url!
}

extension URL {
// Generates an authenticated URL to Mux's license proxy, for a 'license' (a CKC for fairplay),
// for the given playabckID and DRM Token, at the given domain
// - SeeAlso ``init(playbackID:,drmToken:,applicationCertificateLicenseDomain:)``
init(
playbackID: String,
drmToken: String,
licenseDomain: String
) {
let absoluteString = "https://\(licenseDomain)/license/fairplay/\(playbackID)?token=\(drmToken)"
self.init(string: absoluteString)!
}

/// Generates an authenticated URL to Mux's license proxy, for an application certificate, for the
/// given plabackID and DRM token, at the given domain
/// - SeeAlso ``makeLicenseDomain``
static func makeAppCertificateURL(playbackID: String, drmToken: String, licenseDomain: String) -> URL {
let baseStr = "https://\(licenseDomain)/appcert/fairplay/\(playbackID)?token=\(drmToken)"
let url = URL(string: baseStr)
return url!

// Generates an authenticated URL to Mux's license proxy, for an application certificate, for the
// given plabackID and DRM token, at the given domain
// - SeeAlso ``init(playbackID:,drmToken:,licenseDomain: String)``
init(
playbackID: String,
drmToken: String,
applicationCertificateLicenseDomain: String
) {
let absoluteString = "https://\(applicationCertificateLicenseDomain)/appcert/fairplay/\(playbackID)?token=\(drmToken)"
self.init(string: absoluteString)!
}
}

class DefaultFPSSManager: FairPlaySessionManager {

private var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:]
// MARK: - DefaultFairPlayStreamingSessionManager

class DefaultFairPlayStreamingSessionManager<
ContentKeySession: ContentKeyProvider
>: FairPlayStreamingSessionManager {

var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:]
// note - null on simulators or other environments where fairplay isn't supported
private let contentKeySession: AVContentKeySession?
private let sessionDelegate: AVContentKeySessionDelegate?

let contentKeySession: ContentKeySession

var sessionDelegate: AVContentKeySessionDelegate? {
didSet {
contentKeySession.setDelegate(
sessionDelegate,
queue: DispatchQueue(
label: "com.mux.player.fairplay"
)
)
}
}

private let urlSession: URLSession

func addContentKeyRecipient(_ recipient: AVContentKeyRecipient) {
contentKeySession?.addContentKeyRecipient(recipient)
contentKeySession.addContentKeyRecipient(recipient)
}

func removeContentKeyRecipient(_ recipient: AVContentKeyRecipient) {
contentKeySession?.removeContentKeyRecipient(recipient)
contentKeySession.removeContentKeyRecipient(recipient)
}

// MARK: Requesting licenses and certs
Expand All @@ -108,10 +164,12 @@ class DefaultFPSSManager: FairPlaySessionManager {
drmToken: String,
completion requestCompletion: @escaping (Result<Data, Error>) -> Void
) {
let url = DefaultFPSSManager.makeAppCertificateURL(
let url = URL(
playbackID: playbackID,
drmToken: drmToken,
licenseDomain: DefaultFPSSManager.makeLicenseDomain(rootDomain)
applicationCertificateLicenseDomain: String.makeLicenseDomain(
rootDomain: rootDomain
)
)
var request = URLRequest(url: url)
request.httpMethod = "GET"
Expand All @@ -128,7 +186,7 @@ class DefaultFPSSManager: FairPlaySessionManager {
let errorUtf = String(data: errorBody, encoding: .utf8)
print("Cert Error: \(errorUtf ?? "nil")")
}

}
// error case: I/O failed
if let error = error {
Expand Down Expand Up @@ -178,10 +236,12 @@ class DefaultFPSSManager: FairPlaySessionManager {
offline _: Bool,
completion requestCompletion: @escaping (Result<Data, Error>) -> Void
) {
let url = DefaultFPSSManager.makeLicenseURL(
let url = URL(
playbackID: playbackID,
drmToken: drmToken,
licenseDomain: DefaultFPSSManager.makeLicenseDomain(rootDomain)
licenseDomain: String.makeLicenseDomain(
rootDomain: rootDomain
)
)
var request = URLRequest(url: url)

Expand All @@ -196,7 +256,7 @@ class DefaultFPSSManager: FairPlaySessionManager {
print("\t with header fields: \(String(describing: request.allHTTPHeaderFields))")

let task = urlSession.dataTask(with: request) { [requestCompletion] data, response, error in
// error case: I/O failed
// error case: I/O failed
if let error = error {
print("URL Session Task Failed: \(error.localizedDescription)")
requestCompletion(Result.failure(
Expand Down Expand Up @@ -243,13 +303,18 @@ class DefaultFPSSManager: FairPlaySessionManager {
// MARK: registering assets

/// Registers a ``PlaybackOptions`` for DRM playback, associated with the given playbackID
func registerPlaybackOptions(_ opts: PlaybackOptions, for playbackID: String) {
func registerPlaybackOptions(
_ options: PlaybackOptions,
for playbackID: String
) {
print("Registering playbackID \(playbackID)")
playbackOptionsByPlaybackID[playbackID] = opts
playbackOptionsByPlaybackID[playbackID] = options
}

/// Gets a DRM token previously registered via ``registerPlaybackOptions``
func findRegisteredPlaybackOptions(for playbackID: String) -> PlaybackOptions? {
func findRegisteredPlaybackOptions(
for playbackID: String
) -> PlaybackOptions? {
print("Finding playbackID \(playbackID)")
return playbackOptionsByPlaybackID[playbackID]
}
Expand All @@ -259,42 +324,20 @@ class DefaultFPSSManager: FairPlaySessionManager {
print("UN-Registering playbackID \(playbackID)")
playbackOptionsByPlaybackID.removeValue(forKey: playbackID)
}




// MARK: initializers

convenience init() {
#if targetEnvironment(simulator)
let session: AVContentKeySession? = nil
let delegate: AVContentKeySessionDelegate? = nil
#else
let session = AVContentKeySession(keySystem: .fairPlayStreaming)
let delegate = ContentKeySessionDelegate()
#endif

self.init(
contentKeySession: session,
sessionDelegate: delegate,
sessionDelegateQueue: DispatchQueue(label: "com.mux.player.fairplay"),
urlSession: URLSession.shared
)
}


init(
contentKeySession: AVContentKeySession?,
sessionDelegate: AVContentKeySessionDelegate?,
sessionDelegateQueue: DispatchQueue,
contentKeySession: ContentKeySession,
urlSession: URLSession
) {
contentKeySession?.setDelegate(sessionDelegate, queue: sessionDelegateQueue)

self.contentKeySession = contentKeySession
self.sessionDelegate = sessionDelegate
self.urlSession = urlSession
}
}

// MARK: - FairPlaySessionError

enum FairPlaySessionError : Error {
case because(cause: any Error)
case httpFailed(responseStatusCode: Int)
Expand Down
Loading

0 comments on commit c21421d

Please sign in to comment.