Skip to content

Commit

Permalink
chore: dispatch FairPlay-related error details (#57)
Browse files Browse the repository at this point in the history
* chore: add error detail collection hooks to fairplaysession manager

* chore: inject PlayerSDK instance when initializing AVPlayerItem

to ease unit testing

* chore: support multiple observations for one AVPlayer

* player item handle scaffolding

* chore: handle nil inside monitor

* better error instrumentation

* chore: inject playerSDK when preparing existing AVPlayerLayer and AVPlayerViewController

chore: remove unneeded method

* feat: opt-out of automatic error tracking if using FairPlay

* extract playback ID from AVPlayerItem if present

to validate Mux Data configuration

include test fixture variant with stubs that record call arguments

more tests

* pass DRM hint accurately

* Improve defensive copying

Make a defensive copy of MUXSDKCustomerData when it is provided. Since
it is a reference, this prevents alterations the SDK makes to bubble
up to client code. As well as guards against the player software name
and version from being altered after they're set.

Improve readability.

Add customer data tests to validate dimensions are passed through and
check for pointer equality
  • Loading branch information
andrewjl-mux authored Oct 1, 2024
1 parent 858d45c commit 1fd0d34
Show file tree
Hide file tree
Showing 13 changed files with 2,010 additions and 187 deletions.
35 changes: 17 additions & 18 deletions Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,29 @@ class ContentKeySessionDelegate<SessionManager: FairPlayStreamingSessionCredenti
"CK Request Failed Error Localized Description: \(err.localizedDescription)"
)

if let error = err as? NSError {
if let localizedFailureReason = error.localizedFailureReason {
logger.debug(
"CK Request Failed Error Localized Failure Reason: \(localizedFailureReason))"
)
}

let error = err as NSError
if let localizedFailureReason = error.localizedFailureReason {
logger.debug(
"CK Request Failed Error Code: \(error.code)"
"CK Request Failed Error Localized Failure Reason: \(localizedFailureReason))"
)
}

logger.debug(
"CK Request Failed Error Code: \(error.code)"
)

logger.debug(
"CK Request Failed Error User Info: \(error.userInfo)"
)

if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError {
logger.debug(
"CK Request Failed Error User Info: \(error.userInfo)"
"CK Request Failed Underlying Error Localized Description: \(underlyingError.localizedDescription)"
)

if let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? NSError {
logger.debug(
"CK Request Failed Underlying Error Localized Description: \(underlyingError.localizedDescription)"
)

logger.debug(
"CK Request Failed Underlying Error Code: \(underlyingError.code)"
)
}
logger.debug(
"CK Request Failed Underlying Error Code: \(underlyingError.code)"
)
}
}

Expand Down
21 changes: 21 additions & 0 deletions Sources/MuxPlayerSwift/FairPlay/ErrorDispatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// ErrorDispatcher.swift
//
//

import Foundation

protocol ErrorDispatcher {
func dispatchApplicationCertificateRequestError(
error: FairPlaySessionError,
playbackID: String
)

func dispatchLicenseRequestError(
error: FairPlaySessionError,
playbackID: String
)



}
84 changes: 64 additions & 20 deletions Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ class DefaultFairPlayStreamingSessionManager<

var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:]
let contentKeySession: ContentKeySession

let errorDispatcher: (any ErrorDispatcher)

#if DEBUG
var logger: Logger = Logger(
OSLog(
Expand Down Expand Up @@ -142,13 +143,18 @@ class DefaultFairPlayStreamingSessionManager<
logger.debug(
"Invalid FairPlay certificate domain \(rootDomain, privacy: .auto(mask: .hash))"
)
let error = FairPlaySessionError.unexpected(
message: "Invalid certificate domain"
)
requestCompletion(
Result.failure(
FairPlaySessionError.unexpected(
message: "Invalid certificate domain"
)
error
)
)
errorDispatcher.dispatchApplicationCertificateRequestError(
error: error,
playbackID: playbackID
)
return
}
var request = URLRequest(url: url)
Expand Down Expand Up @@ -187,38 +193,53 @@ class DefaultFairPlayStreamingSessionManager<
self.logger.debug(
"Applicate certificate request failed with error: \(error.localizedDescription)"
)
let error = FairPlaySessionError.because(cause: error)
requestCompletion(Result.failure(
FairPlaySessionError.because(cause: error)
error
))
self.errorDispatcher.dispatchApplicationCertificateRequestError(
error: error,
playbackID: playbackID
)
return
}
// error case: I/O finished with non-successful response
guard responseCode == 200 else {
self.logger.debug(
"Applicate certificate request failed with response code: \(String(describing: responseCode))"
)
let error = FairPlaySessionError.httpFailed(
responseStatusCode: responseCode ?? 0
)
requestCompletion(
Result.failure(
FairPlaySessionError.httpFailed(
responseStatusCode: responseCode ?? 0
)
error
)
)
self.errorDispatcher.dispatchApplicationCertificateRequestError(
error: error,
playbackID: playbackID
)
return
}
// this edge case (200 with invalid data) is possible from our DRM vendor
guard let data = data,
data.count > 0 else {
let error = FairPlaySessionError.unexpected(
message: "No cert data with 200 OK response"
)
self.logger.debug(
"Applicate certificate request completed with missing data and response code \(responseCode.debugDescription)"
)
requestCompletion(
Result.failure(
FairPlaySessionError.unexpected(
message: "No cert data with 200 OK respone"
)
error
)
)
self.errorDispatcher.dispatchApplicationCertificateRequestError(
error: error,
playbackID: playbackID
)
return
}

Expand All @@ -245,13 +266,18 @@ class DefaultFairPlayStreamingSessionManager<
drmToken: drmToken,
licenseHostSuffix: rootDomain
).url else {
let error = FairPlaySessionError.unexpected(
message: "Invalid FairPlay license domain"
)
requestCompletion(
Result.failure(
FairPlaySessionError.unexpected(
message: "Invalid FairPlay license domain"
)
error
)
)
errorDispatcher.dispatchLicenseRequestError(
error: error,
playbackID: playbackID
)
return
}

Expand All @@ -273,9 +299,14 @@ class DefaultFairPlayStreamingSessionManager<
self.logger.debug(
"URL Session Task Failed: \(error.localizedDescription)"
)
let error = FairPlaySessionError.because(cause: error)
requestCompletion(Result.failure(
FairPlaySessionError.because(cause: error)
error
))
self.errorDispatcher.dispatchLicenseRequestError(
error: error,
playbackID: playbackID
)
return
}

Expand All @@ -294,11 +325,17 @@ class DefaultFairPlayStreamingSessionManager<
self.logger.debug(
"CKC request failed: \(String(describing: responseCode))"
)
let error = FairPlaySessionError.httpFailed(
responseStatusCode: responseCode ?? 0
)
requestCompletion(Result.failure(
FairPlaySessionError.httpFailed(
responseStatusCode: responseCode ?? 0
)
error
))
self.errorDispatcher.dispatchLicenseRequestError(
error: error,
playbackID: playbackID
)

return
}
// strange edge case: 200 with no response body
Expand All @@ -307,10 +344,15 @@ class DefaultFairPlayStreamingSessionManager<
guard let data = data,
data.count > 0
else {
let error = FairPlaySessionError.unexpected(message: "No license data with 200 response")
self.logger.debug("No CKC data despite server returning success")
requestCompletion(Result.failure(
FairPlaySessionError.unexpected(message: "No license data with 200 response")
error
))
self.errorDispatcher.dispatchLicenseRequestError(
error: error,
playbackID: playbackID
)
return
}

Expand Down Expand Up @@ -349,10 +391,12 @@ class DefaultFairPlayStreamingSessionManager<

init(
contentKeySession: ContentKeySession,
urlSession: URLSession
urlSession: URLSession,
errorDispatcher: any ErrorDispatcher
) {
self.contentKeySession = contentKeySession
self.urlSession = urlSession
self.errorDispatcher = errorDispatcher
}
}

Expand Down
65 changes: 33 additions & 32 deletions Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,32 +28,39 @@ class PlayerSDK {
let fairPlaySessionManager: FairPlayStreamingSessionManager

convenience init() {
let monitor = Monitor()

#if targetEnvironment(simulator)
self.init(
fairPlayStreamingSessionManager: DefaultFairPlayStreamingSessionManager(
contentKeySession: AVContentKeySession(keySystem: .clearKey),
urlSession: .shared
)
urlSession: .shared,
errorDispatcher: monitor
),
monitor: monitor
)
#else
let sessionManager = DefaultFairPlayStreamingSessionManager(
contentKeySession: AVContentKeySession(keySystem: .fairPlayStreaming),
urlSession: .shared
urlSession: .shared,
errorDispatcher: monitor
)
sessionManager.sessionDelegate = ContentKeySessionDelegate(
sessionManager: sessionManager
)
self.init(
fairPlayStreamingSessionManager: sessionManager
fairPlayStreamingSessionManager: sessionManager,
monitor: monitor
)
#endif
}

init(
fairPlayStreamingSessionManager: FairPlayStreamingSessionManager
fairPlayStreamingSessionManager: FairPlayStreamingSessionManager,
monitor: Monitor
) {
self.monitor = Monitor()
self.fairPlaySessionManager = fairPlayStreamingSessionManager
self.monitor = monitor

#if DEBUG
self.abrLogger = Logger(
Expand Down Expand Up @@ -135,6 +142,7 @@ class PlayerSDK {
func registerPlayerLayer(
playerLayer: AVPlayerLayer,
monitoringOptions: MonitoringOptions,
playbackID: String,
requiresReverseProxying: Bool = false,
usingDRM: Bool = false
) {
Expand All @@ -144,6 +152,7 @@ class PlayerSDK {

monitor.setupMonitoring(
playerLayer: playerLayer,
playbackID: playbackID,
options: monitoringOptions,
usingDRM: usingDRM
)
Expand All @@ -163,6 +172,7 @@ class PlayerSDK {
func registerPlayerViewController(
playerViewController: AVPlayerViewController,
monitoringOptions: MonitoringOptions,
playbackID: String,
requiresReverseProxying: Bool = false,
usingDRM: Bool = false
) {
Expand All @@ -172,6 +182,7 @@ class PlayerSDK {

monitor.setupMonitoring(
playerViewController: playerViewController,
playbackID: playbackID,
options: monitoringOptions,
usingDRM: usingDRM
)
Expand Down Expand Up @@ -224,47 +235,37 @@ class PlayerSDK {
}

class KeyValueObservation {
var observations: [ObjectIdentifier: NSKeyValueObservation] = [:]
var observations: [ObjectIdentifier: Set<NSKeyValueObservation>] = [:]

func register<Value>(
_ player: AVPlayer,
for keyPath: KeyPath<AVPlayer, Value>,
options: NSKeyValueObservingOptions,
changeHandler: @escaping (AVPlayer, NSKeyValueObservedChange<Value>) -> Void
) {
let observation = player.observe(keyPath,
options: options,
changeHandler: changeHandler
let observation = player.observe(
keyPath,
options: options,
changeHandler: changeHandler
)
observations[ObjectIdentifier(player)] = observation

if var o = observations[ObjectIdentifier(player)] {
o.insert(observation)
observations[ObjectIdentifier(player)] = o
} else {
observations[ObjectIdentifier(player)] = Set(arrayLiteral: observation)
}
}

func unregister(
_ player: AVPlayer
) {
if let observation = observations[ObjectIdentifier(player)] {
observation.invalidate()
if let o = observations[ObjectIdentifier(player)] {
o.forEach { observation in
observation.invalidate()
}
observations.removeValue(forKey: ObjectIdentifier(player))
}
}
}
}

// MARK extension for observations for DRM
extension PlayerSDK {
func observePlayerForDRM(_ player: AVPlayer) {
keyValueObservation.register(
player,
for: \AVPlayer.currentItem,
options: [.old, .new]
) { player, change in
if let oldAsset = change.oldValue??.asset as? AVURLAsset {
PlayerSDK.shared.fairPlaySessionManager.removeContentKeyRecipient(oldAsset)
}
}
}

func stopObservingPlayerForDrm(_ player: AVPlayer) {
keyValueObservation.unregister(player)
}
}
Loading

0 comments on commit 1fd0d34

Please sign in to comment.