Skip to content

Commit

Permalink
maint: Add Unit Tests for FairPlaySessionManager + good DRM errors + …
Browse files Browse the repository at this point in the history
…small testability changes (#40)

* Ok here we go

* different URLSessions

* Fairplay -> FairPlay

* move helpers out of the protocol

* root domain testable

* file for fpssm tests

* warnings

* there we go. one test

* Rename FairPlaySessionManagerImpl to DefaultFPSMangaer

* Ok that's the trivial tests

* do the URLProtcol thing

* Mock URLSession

* Test success case for requestLicense

* Errors

* Error

* ok there's license almost done

* some tests

* Tests added

* So far so good

* now throw the right errors

* Added tests, need to update license error returns

* Ok now that's some good error handling

* Finished Cert and license tests

* test app cert request

* no more warnings

* Ok removed TempError

* cleanup a little

* little note

* Cleanup more

* Oops forgot ckc request headers

* refactor: redirect singleton calls to injected dependencies

refactor: place AVContentKeySession behind a protocol

build: fix build issues and tests

test setup

fix test

inject correctly

remove empty extension and avoid docc style comments for internal methods

remove optionals

* Rename abbreviated identifiers and split apart protocols

* Remove test session delegate

* turns out we do not need this

* refactor: minimize implicit singleton calls

* test: validate DRM registration when drm token provided

* Non-public API, use extension to initialize URL

* avoid tripping up docc for internal extensions

* cleaner test

* route calls to one chokepoint for simplicity

* We can use generics instead I think

* add a few comments

* some comments

* some comments

* Need to set ContentKeyDelegate

* Tests for ContentKeySessionDelegate + Testability Tweaks (#44)

* Extract AVContentKeyRequest protocol

* remove ContentKeyDelegate methods we're not using

* Hook in KeyRequest

* record some mocks

* ok this is what we can do

* here is something

* Something more

* now we are somewhere

* lol

* setting up

* now we have something goin

* errors

* there we go

* tests

* more error

* more tests

* Content key path

* happy path

* more

* Now that works

* now we are somewhere

* PR Comment: back to one SessionManager

* PR Comments: back to SessionMangaer

* And the tests

* PR comments: make some stuff optional

* nit

---------

Co-authored-by: AJ Lauer Barinov <abarinov@mux.com>
  • Loading branch information
daytime-em and andrewjl-mux authored May 8, 2024
1 parent 7dc2608 commit b709995
Show file tree
Hide file tree
Showing 16 changed files with 1,650 additions and 310 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XX95P4Y787;
DEVELOPMENT_TEAM = CX6AHWLHM6;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mux.player.example.MuxPlayerSwiftExample.MuxPlayerSwiftExampleUITests;
Expand All @@ -569,7 +569,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XX95P4Y787;
DEVELOPMENT_TEAM = CX6AHWLHM6;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mux.player.example.MuxPlayerSwiftExample.MuxPlayerSwiftExampleUITests;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,39 @@
import Foundation
import AVFoundation

class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
class ContentKeySessionDelegate<SessionManager: FairPlayStreamingSessionCredentialClient & PlaybackOptionsRegistry> : 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: DefaultKeyRequest(wrapping: keyRequest))
}

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

func contentKeySession(_ session: AVContentKeySession, contentKeyRequestDidSucceed keyRequest: AVContentKeyRequest) {
// this func intentionally left blank
// TODO: Log more nicely (ie, with a Logger)
print("CKC Request Success")
}

func contentKeySession(_ session: AVContentKeySession, contentKeyRequest keyRequest: AVContentKeyRequest, didFailWithError err: any Error) {
// TODO: Log more nicely (ie, with a Logger)
print("CKC Request Failed!!! \(err.localizedDescription)")
}

func contentKeySessionContentProtectionSessionIdentifierDidChange(_ session: AVContentKeySession) {
print("Content Key session ID changed apparently")
}

func contentKeySessionDidGenerateExpiredSessionReport(_ session: AVContentKeySession) {
print("Expired session report generated (whatever that means)")
}

func contentKeySession(_ session: AVContentKeySession, externalProtectionStatusDidChangeFor contentKey: AVContentKey) {
print("External Protection status changed for a content key sesison")
}

func contentKeySession(_ session: AVContentKeySession, shouldRetry keyRequest: AVContentKeyRequest,
reason retryReason: AVContentKeyRequest.RetryReason) -> Bool {
print("===shouldRetry called with reason \(retryReason)")
// TODO: use Logger
print("shouldRetry called with reason \(retryReason)")

var shouldRetry = false

Expand Down Expand Up @@ -79,8 +76,8 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
// MARK: Logic

func parsePlaybackId(fromSkdLocation uri: URL) -> String? {
// pull the playbackID out of the uri to the key
let urlComponents = URLComponents(url: uri, resolvingAgainstBaseURL: false)
// pull the playbackID out of the uri to the key
let urlComponents = URLComponents(url: uri, resolvingAgainstBaseURL: false)
guard let urlComponents = urlComponents else {
// not likely
print("!! Error: Cannot Parse URI")
Expand All @@ -95,7 +92,7 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
return playbackID
}

func handleContentKeyRequest(_ session: AVContentKeySession, request: AVContentKeyRequest) {
func handleContentKeyRequest(request: any KeyRequest) {
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 @@ -108,39 +105,61 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {

let playbackID = parsePlaybackId(fromSkdLocation: keyURL)
guard let playbackID = playbackID else {
print("No playbackID found from server , aborting")
request.processContentKeyResponseError(
FairPlaySessionError.unexpected(
message: "playbackID not present in key uri"
)
)
return
}

let playbackOptions = PlayerSDK.shared.fairPlaySessionManager
.findRegisteredPlaybackOptions(for: playbackID)
guard let sessionManager = self.sessionManager else {
print("no 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")
request.processContentKeyResponseError(
FairPlaySessionError.unexpected(
message: "Token was not registered, only happens during SDK errors"
)
)
return
}

let rootDomain = playbackOptions.customDomain ?? "mux.com"
let rootDomain = playbackOptions.rootDomain()

// get app cert
var applicationCertificate: Data?
var appCertError: (any Error)?
// 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,
completion: { result in
if let cert = try? result.get() {
applicationCertificate = cert
do {
applicationCertificate = try result.get()
} catch {
appCertError = error
}
group.leave()
}
)
group.wait()
guard let applicationCertificate = applicationCertificate else {
print("failed to get application certificate")
request.processContentKeyResponseError(
FairPlaySessionError.because(
cause: appCertError!
)
)
return
}

Expand All @@ -154,9 +173,9 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
}

guard let spcData = spcData else {
print("No SPC Data in spc response")
// `error` will be non-nil by contract
request.processContentKeyResponseError(error!)
request.processContentKeyResponseError(
error ?? FairPlaySessionError.unexpected(message: "no SPC")
)
return
}

Expand All @@ -171,18 +190,23 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
}
}

private func handleSpcObtainedFromCDM(
func handleSpcObtainedFromCDM(
spcData: Data,
playbackID: String,
drmToken: String,
rootDomain: String, // without any "license." or "stream." prepended, eg mux.com, custom.1234.co.uk
request: AVContentKeyRequest
request: any KeyRequest
) {
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 All @@ -197,19 +221,80 @@ class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate {
group.wait()

guard let ckcData = ckcData else {
print("no CKC Data in CKC response")
request.processContentKeyResponseError(TempError())
request.processContentKeyResponseError(FairPlaySessionError.unexpected(message: "No CKC Data returned from CDM"))
return
}

print("Submitting CKC to system")
// Send CKC to CDM/wherever else so we can finally play our content
let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: ckcData)
// let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: ckcData)
let keyResponse = request.makeContentKeyResponse(data: ckcData)
request.processContentKeyResponse(keyResponse)
// Done! no further interaction is required from us to play.
}
}

// Wraps a generic request for a key and delegates calls to it
// this protocol's methods are intended to match AVContentKeyRequest
protocol KeyRequest {

associatedtype InnerRequest

var identifier: Any? { get }

func makeContentKeyResponse(data: Data) -> AVContentKeyResponse

func processContentKeyResponse(_ response: AVContentKeyResponse)
func processContentKeyResponseError(_ error: any Error)
func makeStreamingContentKeyRequestData(forApp appIdentifier: Data,
contentIdentifier: Data?,
options: [String : Any]?,
completionHandler handler: @escaping (Data?, (any Error)?) -> Void)
}

// Wraps a real AVContentKeyRequest and straightforwardly delegates to it
struct DefaultKeyRequest : KeyRequest {
typealias InnerRequest = AVContentKeyRequest

var identifier: Any? {
get {
return self.request.identifier
}
}

func makeContentKeyResponse(data: Data) -> AVContentKeyResponse {
return AVContentKeyResponse(fairPlayStreamingKeyResponseData: data)
}

func processContentKeyResponse(_ response: AVContentKeyResponse) {
self.request.processContentKeyResponse(response)
}

func processContentKeyResponseError(_ error: any Error) {
self.request.processContentKeyResponseError(error)
}

func makeStreamingContentKeyRequestData(
forApp appIdentifier: Data,
contentIdentifier: Data?,
options: [String : Any]? = nil,
completionHandler handler: @escaping (Data?, (any Error)?) -> Void
) {
self.request.makeStreamingContentKeyRequestData(
forApp: appIdentifier,
contentIdentifier: contentIdentifier,
options: options,
completionHandler: handler
)
}

let request: InnerRequest

init(wrapping request: InnerRequest) {
self.request = request
}
}

extension URLComponents {
func findQueryValue(key: String) -> String? {
if let items = self.queryItems {
Expand Down
Loading

0 comments on commit b709995

Please sign in to comment.