Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: dispatch FairPlay-related error details #57

Merged
merged 32 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2cac72c
chore: add error detail collection hooks to fairplaysession manager
andrewjl-mux Jul 24, 2024
462b0e7
chore: inject PlayerSDK instance when initializing AVPlayerItem
andrewjl-mux Aug 26, 2024
a8ffcce
chore: support multiple observations for one AVPlayer
andrewjl-mux Aug 26, 2024
a320faa
chore: remove unused code (should this be called somewhere?)
andrewjl-mux Aug 26, 2024
41c70ba
player item handle scaffolding
andrewjl-mux Aug 26, 2024
34a8d58
chore: handle nil inside monitor
andrewjl-mux Aug 27, 2024
179f131
better error instrumentation
andrewjl-mux Aug 27, 2024
901ab89
chore: inject playerSDK when preparing existing AVPlayerLayer and AVP…
andrewjl-mux Aug 27, 2024
530fdf6
chore: thread playbackID through
andrewjl-mux Aug 27, 2024
095c04b
fix tests
andrewjl-mux Aug 27, 2024
cda0611
feat: opt-out of automatic error tracking if using FairPlay
andrewjl-mux Aug 28, 2024
1d93657
extract playback ID from AVPlayerItem if present
andrewjl-mux Sep 4, 2024
66f355e
record player object identifier
andrewjl-mux Sep 6, 2024
7139cde
this makes more sense here
andrewjl-mux Sep 6, 2024
d5fe475
weak references
andrewjl-mux Sep 6, 2024
fd76c03
move current item observation
andrewjl-mux Sep 6, 2024
5dfe61f
selective error fwding
andrewjl-mux Sep 6, 2024
77003fa
property
andrewjl-mux Sep 6, 2024
d156dbc
mappings
andrewjl-mux Sep 6, 2024
9724a90
invert mapping to simplify routing error to the right player
andrewjl-mux Sep 6, 2024
9116546
typo
andrewjl-mux Sep 6, 2024
77a87b2
more descriptive names
andrewjl-mux Sep 6, 2024
669cb64
add context with comment
andrewjl-mux Sep 6, 2024
654916b
fix warning
andrewjl-mux Sep 6, 2024
8a44c80
apply and fix stash
andrewjl-mux Sep 9, 2024
54efc16
tests
andrewjl-mux Sep 9, 2024
6dc6f9f
pass DRM hint accurately
andrewjl-mux Sep 11, 2024
dddf653
even more tests
andrewjl-mux Sep 11, 2024
3dcc32d
DRYer tests
andrewjl-mux Sep 11, 2024
24d4b76
Improve defensive copying
andrewjl-mux Sep 11, 2024
d19ab54
tests
andrewjl-mux Sep 12, 2024
45f1917
generic error message
andrewjl-mux Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading