From 2494757d1a78a6070a4f9129aa0b45f90f98fe48 Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 15:30:25 -0700 Subject: [PATCH 01/13] 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 --- .../FairPlay/ContentKeySessionDelegate.swift | 37 +++- .../FairPlay/FairPlaySessionManager.swift | 194 ++++++++++++------ .../GlobalLifecycle/PlayerSDK.swift | 7 +- .../FairPlaySessionManagerTests.swift | 31 +-- .../DummyAVContentKeySessionDelegate.swift | 0 .../{TestUtils => Helpers}/FakeError.swift | 0 .../MockURLProtocol.swift | 0 .../Helpers/TestContentKeySession.swift | 35 ++++ 8 files changed, 222 insertions(+), 82 deletions(-) rename Tests/MuxPlayerSwift/{TestUtils => Helpers}/DummyAVContentKeySessionDelegate.swift (100%) rename Tests/MuxPlayerSwift/{TestUtils => Helpers}/FakeError.swift (100%) rename Tests/MuxPlayerSwift/{TestUtils => Helpers}/MockURLProtocol.swift (100%) create mode 100644 Tests/MuxPlayerSwift/Helpers/TestContentKeySession.swift diff --git a/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift b/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift index 812db100..e453d312 100644 --- a/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift +++ b/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift @@ -8,16 +8,24 @@ import Foundation import AVFoundation -class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate { - +class ContentKeySessionDelegate : 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) { @@ -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, @@ -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") @@ -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, @@ -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, diff --git a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift index 41a96771..d9b47cd1 100644 --- a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift +++ b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift @@ -8,8 +8,10 @@ import Foundation import AVFoundation -protocol FairPlaySessionManager { - +// MARK: - FairPlaySessionManager + +protocol FairPlaySessionManager: AnyObject { + // MARK: Requesting licenses and certs /// Requests the App Certificate for a playback id @@ -45,16 +47,60 @@ protocol FairPlaySessionManager { func unregisterPlaybackOptions(for playbackID: String) } +// 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 { } + +// TODO: make this functional, if necessary +class ClearContentKeyProvider { + var delegate: (any AVContentKeySessionDelegate)? + var delegateQueue: dispatch_queue_t? +} + +extension ClearContentKeyProvider: ContentKeyProvider { + func setDelegate( + _ delegate: (any AVContentKeySessionDelegate)?, + queue delegateQueue: dispatch_queue_t? + ) { + self.delegate = delegate + self.delegateQueue = delegateQueue + } + + func addContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { + // no-op + } + + func removeContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { + // no-op + } +} + +// MARK: - DefaultFPSSManager + // 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" @@ -62,33 +108,51 @@ extension DefaultFPSSManager { 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 { - +class DefaultFPSSManager: FairPlaySessionManager { + private var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:] // note - null on simulators or other environments where fairplay isn't supported - private let contentKeySession: AVContentKeySession? - private let sessionDelegate: AVContentKeySessionDelegate? - + private 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) { @@ -108,10 +172,12 @@ class DefaultFPSSManager: FairPlaySessionManager { drmToken: String, completion requestCompletion: @escaping (Result) -> 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" @@ -128,7 +194,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 { @@ -178,10 +244,12 @@ class DefaultFPSSManager: FairPlaySessionManager { offline _: Bool, completion requestCompletion: @escaping (Result) -> 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) @@ -196,7 +264,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( @@ -263,38 +331,46 @@ class DefaultFPSSManager: FairPlaySessionManager { // 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 } } +internal extension DefaultFPSSManager where ContentKeySession == ClearContentKeyProvider { + convenience init() { + self.init( + contentKeySession: ClearContentKeyProvider(), + urlSession: URLSession.shared + ) + let delegate = ContentKeySessionDelegate( + sessionManager: self + ) + self.sessionDelegate = delegate + } +} + +internal extension DefaultFPSSManager where ContentKeySession == AVContentKeySession { + convenience init() { + self.init( + contentKeySession: AVContentKeySession( + keySystem: .fairPlayStreaming + ), + urlSession: URLSession.shared + ) + let delegate = ContentKeySessionDelegate( + sessionManager: self + ) + self.sessionDelegate = delegate + } +} + +// MARK: - FairPlaySessionError + enum FairPlaySessionError : Error { case because(cause: any Error) case httpFailed(responseStatusCode: Int) diff --git a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift index 79a8e793..7f151d93 100644 --- a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift +++ b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift @@ -23,7 +23,12 @@ class PlayerSDK { init() { self.monitor = Monitor() self.keyValueObservation = KeyValueObservation() - self.fairPlaySessionManager = DefaultFPSSManager() + + #if targetEnvironment(simulator) + self.fairPlaySessionManager = DefaultFPSSManager() + #else + self.fairPlaySessionManager = DefaultFPSSManager() + #endif } class KeyValueObservation { diff --git a/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift b/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift index b0b607bb..14b48dd9 100644 --- a/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift +++ b/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift @@ -14,32 +14,35 @@ class FairPlaySessionManagerTests : XCTestCase { // mocks private var mockURLSession: URLSession! - private var mockAVContentKeySession: DummyAVContentKeySession! + // object under test private var sessionManager: FairPlaySessionManager! override func setUp() { super.setUp() - let mockURLSessionConfig = URLSessionConfiguration.default mockURLSessionConfig.protocolClasses = [MockURLProtocol.self] self.mockURLSession = URLSession.init(configuration: mockURLSessionConfig) - - self.mockAVContentKeySession = DummyAVContentKeySession(keySystem: .clearKey) - self.sessionManager = DefaultFPSSManager( + let session = TestContentKeySession() + let defaultFairPlaySessionManager = DefaultFPSSManager( // .clearKey is used because .fairPlay requires a physical device - contentKeySession: mockAVContentKeySession, - sessionDelegate: DummyAVContentKeySessionDelegate(), - sessionDelegateQueue: DispatchQueue(label: "com.mux.player.test.fairplay"), + contentKeySession: session, urlSession: mockURLSession ) + self.sessionManager = defaultFairPlaySessionManager + defaultFairPlaySessionManager.sessionDelegate = ContentKeySessionDelegate( + sessionManager: defaultFairPlaySessionManager + ) + } // Also tests PlaybackOptions.rootDomain func testMakeLicenseDomain() throws { let optionsWithoutCustomDomain = PlaybackOptions() - let defaultLicenseDomain = DefaultFPSSManager.makeLicenseDomain(optionsWithoutCustomDomain.rootDomain()) + let defaultLicenseDomain = String.makeLicenseDomain( + rootDomain: optionsWithoutCustomDomain.rootDomain() + ) XCTAssert( defaultLicenseDomain == "license.mux.com", "Default license server is license.mux.com" @@ -47,7 +50,9 @@ class FairPlaySessionManagerTests : XCTestCase { var optionsCustomDomain = PlaybackOptions() optionsCustomDomain.customDomain = "fake.custom.domain.xyz" - let customLicenseDomain = DefaultFPSSManager.makeLicenseDomain(optionsCustomDomain.rootDomain()) + let customLicenseDomain = String.makeLicenseDomain( + rootDomain: optionsCustomDomain.rootDomain() + ) XCTAssert( customLicenseDomain == "license.fake.custom.domain.xyz", "Custom license server is license.fake.custom.domain.xyz" @@ -59,7 +64,7 @@ class FairPlaySessionManagerTests : XCTestCase { let fakeDrmToken = "fake_drm_token" let fakeLicenseDomain = "license.fake.domain.xyz" - let licenseURL = DefaultFPSSManager.makeLicenseURL( + let licenseURL = URL( playbackID: fakePlaybackId, drmToken: fakeDrmToken, licenseDomain: fakeLicenseDomain @@ -76,10 +81,10 @@ class FairPlaySessionManagerTests : XCTestCase { let fakeDrmToken = "fake_drm_token" let fakeLicenseDomain = "license.fake.domain.xyz" - let licenseURL = DefaultFPSSManager.makeAppCertificateURL( + let licenseURL = URL( playbackID: fakePlaybackId, drmToken: fakeDrmToken, - licenseDomain: fakeLicenseDomain + applicationCertificateLicenseDomain: fakeLicenseDomain ) let expected = "https://\(fakeLicenseDomain)/appcert/fairplay/\(fakePlaybackId)?token=\(fakeDrmToken)" diff --git a/Tests/MuxPlayerSwift/TestUtils/DummyAVContentKeySessionDelegate.swift b/Tests/MuxPlayerSwift/Helpers/DummyAVContentKeySessionDelegate.swift similarity index 100% rename from Tests/MuxPlayerSwift/TestUtils/DummyAVContentKeySessionDelegate.swift rename to Tests/MuxPlayerSwift/Helpers/DummyAVContentKeySessionDelegate.swift diff --git a/Tests/MuxPlayerSwift/TestUtils/FakeError.swift b/Tests/MuxPlayerSwift/Helpers/FakeError.swift similarity index 100% rename from Tests/MuxPlayerSwift/TestUtils/FakeError.swift rename to Tests/MuxPlayerSwift/Helpers/FakeError.swift diff --git a/Tests/MuxPlayerSwift/TestUtils/MockURLProtocol.swift b/Tests/MuxPlayerSwift/Helpers/MockURLProtocol.swift similarity index 100% rename from Tests/MuxPlayerSwift/TestUtils/MockURLProtocol.swift rename to Tests/MuxPlayerSwift/Helpers/MockURLProtocol.swift diff --git a/Tests/MuxPlayerSwift/Helpers/TestContentKeySession.swift b/Tests/MuxPlayerSwift/Helpers/TestContentKeySession.swift new file mode 100644 index 00000000..67f2241b --- /dev/null +++ b/Tests/MuxPlayerSwift/Helpers/TestContentKeySession.swift @@ -0,0 +1,35 @@ +// +// TestContentKeySession.swift +// +// +// Created by Emily Dixon on 5/2/24. +// + +import Foundation +import AVKit + +@testable import MuxPlayerSwift + +class TestContentKeySession: ContentKeyProvider { + + var delegate: (any AVContentKeySessionDelegate)? + + func setDelegate( + _ delegate: (any AVContentKeySessionDelegate)?, + queue delegateQueue: dispatch_queue_t? + ) { + self.delegate = delegate + } + + func addContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { + + } + + func removeContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { + + } + + init() { + + } +} From 7ce571f4ecc1ddccfdea737349b223283caa935a Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 17:07:55 -0700 Subject: [PATCH 02/13] Rename abbreviated identifiers and split apart protocols --- .../FairPlay/ContentKeySessionDelegate.swift | 2 +- .../FairPlay/FairPlaySessionManager.swift | 47 ++++++++++--------- .../GlobalLifecycle/PlayerSDK.swift | 6 +-- .../FairPlaySessionManagerTests.swift | 4 +- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift b/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift index e453d312..2a5a6f7d 100644 --- a/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift +++ b/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift @@ -8,7 +8,7 @@ import Foundation import AVFoundation -class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate { +class ContentKeySessionDelegate : NSObject, AVContentKeySessionDelegate { weak var sessionManager: SessionManager? diff --git a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift index d9b47cd1..8495f2ac 100644 --- a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift +++ b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift @@ -8,21 +8,20 @@ import Foundation import AVFoundation -// MARK: - FairPlaySessionManager - -protocol FairPlaySessionManager: AnyObject { +// MARK: - FairPlayStreamingSessionManager +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) -> 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, @@ -31,14 +30,9 @@ protocol FairPlaySessionManager: AnyObject { offline _: Bool, completion requestCompletion: @escaping (Result) -> 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) +} + +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`` @@ -47,6 +41,17 @@ protocol FairPlaySessionManager: AnyObject { func unregisterPlaybackOptions(for playbackID: String) } +// MARK: registering drm-protected assets +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) +} + +typealias FairPlayStreamingSessionManager = FairPlayStreamingSessionCredentialClient & PlaybackOptionsRegistry & ContentKeyRecipientRegistry + // MARK: - Content Key Provider // Define protocol for calls made to AVContentKeySession @@ -89,7 +94,7 @@ extension ClearContentKeyProvider: ContentKeyProvider { } } -// MARK: - DefaultFPSSManager +// MARK: - DefaultFairPlayStreamingSessionManager // MARK: helpers for interacting with the license server @@ -136,7 +141,7 @@ extension URL { } } -class DefaultFPSSManager: FairPlaySessionManager { +class DefaultFairPlayStreamingSessionManager: FairPlayStreamingSessionManager { private var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:] // note - null on simulators or other environments where fairplay isn't supported @@ -327,9 +332,7 @@ class DefaultFPSSManager: FairPlaySession print("UN-Registering playbackID \(playbackID)") playbackOptionsByPlaybackID.removeValue(forKey: playbackID) } - - - + // MARK: initializers init( @@ -341,7 +344,7 @@ class DefaultFPSSManager: FairPlaySession } } -internal extension DefaultFPSSManager where ContentKeySession == ClearContentKeyProvider { +internal extension DefaultFairPlayStreamingSessionManager where ContentKeySession == ClearContentKeyProvider { convenience init() { self.init( contentKeySession: ClearContentKeyProvider(), @@ -354,7 +357,7 @@ internal extension DefaultFPSSManager where ContentKeySession == ClearContentKey } } -internal extension DefaultFPSSManager where ContentKeySession == AVContentKeySession { +internal extension DefaultFairPlayStreamingSessionManager where ContentKeySession == AVContentKeySession { convenience init() { self.init( contentKeySession: AVContentKeySession( diff --git a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift index 7f151d93..ec43c8af 100644 --- a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift +++ b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift @@ -18,16 +18,16 @@ class PlayerSDK { let keyValueObservation: KeyValueObservation - let fairPlaySessionManager: FairPlaySessionManager + let fairPlaySessionManager: FairPlayStreamingSessionManager init() { self.monitor = Monitor() self.keyValueObservation = KeyValueObservation() #if targetEnvironment(simulator) - self.fairPlaySessionManager = DefaultFPSSManager() + self.fairPlaySessionManager = DefaultFairPlayStreamingSessionManager() #else - self.fairPlaySessionManager = DefaultFPSSManager() + self.fairPlaySessionManager = DefaultFairPlayStreamingSessionManager() #endif } diff --git a/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift b/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift index 14b48dd9..9ee863f3 100644 --- a/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift +++ b/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift @@ -17,7 +17,7 @@ class FairPlaySessionManagerTests : XCTestCase { // object under test - private var sessionManager: FairPlaySessionManager! + private var sessionManager: FairPlayStreamingSessionManager! override func setUp() { super.setUp() @@ -25,7 +25,7 @@ class FairPlaySessionManagerTests : XCTestCase { mockURLSessionConfig.protocolClasses = [MockURLProtocol.self] self.mockURLSession = URLSession.init(configuration: mockURLSessionConfig) let session = TestContentKeySession() - let defaultFairPlaySessionManager = DefaultFPSSManager( + let defaultFairPlaySessionManager = DefaultFairPlayStreamingSessionManager( // .clearKey is used because .fairPlay requires a physical device contentKeySession: session, urlSession: mockURLSession From d667eb290d5dc0aba673315bc51ab80fcd42222f Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 17:16:33 -0700 Subject: [PATCH 03/13] Remove test session delegate --- .../FairPlay/FairPlaySessionManager.swift | 19 +++++++++++++------ .../DummyAVContentKeySessionDelegate.swift | 18 ------------------ 2 files changed, 13 insertions(+), 24 deletions(-) delete mode 100644 Tests/MuxPlayerSwift/Helpers/DummyAVContentKeySessionDelegate.swift diff --git a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift index 8495f2ac..32fe8017 100644 --- a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift +++ b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift @@ -94,8 +94,6 @@ extension ClearContentKeyProvider: ContentKeyProvider { } } -// MARK: - DefaultFairPlayStreamingSessionManager - // MARK: helpers for interacting with the license server extension String { @@ -141,7 +139,11 @@ extension URL { } } -class DefaultFairPlayStreamingSessionManager: FairPlayStreamingSessionManager { +// MARK: - DefaultFairPlayStreamingSessionManager + +class DefaultFairPlayStreamingSessionManager< + ContentKeySession: ContentKeyProvider +>: FairPlayStreamingSessionManager { private var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:] // note - null on simulators or other environments where fairplay isn't supported @@ -316,13 +318,18 @@ class DefaultFairPlayStreamingSessionManager PlaybackOptions? { + func findRegisteredPlaybackOptions( + for playbackID: String + ) -> PlaybackOptions? { print("Finding playbackID \(playbackID)") return playbackOptionsByPlaybackID[playbackID] } diff --git a/Tests/MuxPlayerSwift/Helpers/DummyAVContentKeySessionDelegate.swift b/Tests/MuxPlayerSwift/Helpers/DummyAVContentKeySessionDelegate.swift deleted file mode 100644 index 6dcb1c09..00000000 --- a/Tests/MuxPlayerSwift/Helpers/DummyAVContentKeySessionDelegate.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// File.swift -// -// -// Created by Emily Dixon on 5/2/24. -// - -import Foundation -import AVKit - -/// Dummy AVContentKeySessionDelegate. Doesn't respond to calls or do anything -class DummyAVContentKeySessionDelegate: NSObject, AVContentKeySessionDelegate { - - func contentKeySession(_ session: AVContentKeySession, didProvide keyRequest: AVContentKeyRequest) { - // no op - } - -} From 0db869e4e07298a033916b2318f4f2e38792e232 Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 17:46:38 -0700 Subject: [PATCH 04/13] turns out we do not need this --- .../FairPlay/FairPlaySessionManager.swift | 52 ------------------- .../GlobalLifecycle/PlayerSDK.swift | 10 +++- .../Helpers/TestContentKeySession.swift | 6 ++- 3 files changed, 12 insertions(+), 56 deletions(-) diff --git a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift index 32fe8017..47090623 100644 --- a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift +++ b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift @@ -70,30 +70,6 @@ protocol ContentKeyProvider { // these methods so this declaration can be empty extension AVContentKeySession: ContentKeyProvider { } -// TODO: make this functional, if necessary -class ClearContentKeyProvider { - var delegate: (any AVContentKeySessionDelegate)? - var delegateQueue: dispatch_queue_t? -} - -extension ClearContentKeyProvider: ContentKeyProvider { - func setDelegate( - _ delegate: (any AVContentKeySessionDelegate)?, - queue delegateQueue: dispatch_queue_t? - ) { - self.delegate = delegate - self.delegateQueue = delegateQueue - } - - func addContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { - // no-op - } - - func removeContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { - // no-op - } -} - // MARK: helpers for interacting with the license server extension String { @@ -351,34 +327,6 @@ class DefaultFairPlayStreamingSessionManager< } } -internal extension DefaultFairPlayStreamingSessionManager where ContentKeySession == ClearContentKeyProvider { - convenience init() { - self.init( - contentKeySession: ClearContentKeyProvider(), - urlSession: URLSession.shared - ) - let delegate = ContentKeySessionDelegate( - sessionManager: self - ) - self.sessionDelegate = delegate - } -} - -internal extension DefaultFairPlayStreamingSessionManager where ContentKeySession == AVContentKeySession { - convenience init() { - self.init( - contentKeySession: AVContentKeySession( - keySystem: .fairPlayStreaming - ), - urlSession: URLSession.shared - ) - let delegate = ContentKeySessionDelegate( - sessionManager: self - ) - self.sessionDelegate = delegate - } -} - // MARK: - FairPlaySessionError enum FairPlaySessionError : Error { diff --git a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift index ec43c8af..3109c192 100644 --- a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift +++ b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift @@ -25,9 +25,15 @@ class PlayerSDK { self.keyValueObservation = KeyValueObservation() #if targetEnvironment(simulator) - self.fairPlaySessionManager = DefaultFairPlayStreamingSessionManager() + self.fairPlaySessionManager = DefaultFairPlayStreamingSessionManager( + contentKeySession: AVContentKeySession(keySystem: .clearKey), + urlSession: .shared + ) #else - self.fairPlaySessionManager = DefaultFairPlayStreamingSessionManager() + self.fairPlaySessionManager = DefaultFairPlayStreamingSessionManager( + contentKeySession: AVContentKeySession(keySystem: .fairPlayStreaming), + urlSession: .shared + ) #endif } diff --git a/Tests/MuxPlayerSwift/Helpers/TestContentKeySession.swift b/Tests/MuxPlayerSwift/Helpers/TestContentKeySession.swift index 67f2241b..322c0bf3 100644 --- a/Tests/MuxPlayerSwift/Helpers/TestContentKeySession.swift +++ b/Tests/MuxPlayerSwift/Helpers/TestContentKeySession.swift @@ -14,6 +14,8 @@ class TestContentKeySession: ContentKeyProvider { var delegate: (any AVContentKeySessionDelegate)? + var contentKeyRecipients: [any AVContentKeyRecipient] = [] + func setDelegate( _ delegate: (any AVContentKeySessionDelegate)?, queue delegateQueue: dispatch_queue_t? @@ -22,11 +24,11 @@ class TestContentKeySession: ContentKeyProvider { } func addContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { - + contentKeyRecipients.append(recipient) } func removeContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { - + // no-op } init() { From f114f2e59eaf28e43aba2acfc479de075191a64d Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 18:35:17 -0700 Subject: [PATCH 05/13] refactor: minimize implicit singleton calls --- .../FairPlay/FairPlaySessionManager.swift | 10 ++-- .../GlobalLifecycle/PlayerSDK.swift | 48 +++++++++++++++---- .../Extensions/AVPlayerItem+Mux.swift | 47 +++++++++++------- 3 files changed, 74 insertions(+), 31 deletions(-) diff --git a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift index 47090623..8e9ba3c5 100644 --- a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift +++ b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift @@ -121,13 +121,13 @@ class DefaultFairPlayStreamingSessionManager< ContentKeySession: ContentKeyProvider >: FairPlayStreamingSessionManager { - private var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:] + var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:] // note - null on simulators or other environments where fairplay isn't supported - private let contentKeySession: ContentKeySession? + let contentKeySession: ContentKeySession var sessionDelegate: AVContentKeySessionDelegate? { didSet { - contentKeySession?.setDelegate( + contentKeySession.setDelegate( sessionDelegate, queue: DispatchQueue( label: "com.mux.player.fairplay" @@ -139,11 +139,11 @@ class DefaultFairPlayStreamingSessionManager< 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 diff --git a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift index 3109c192..4c44b8f5 100644 --- a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift +++ b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift @@ -20,23 +20,51 @@ class PlayerSDK { let fairPlaySessionManager: FairPlayStreamingSessionManager - init() { - self.monitor = Monitor() - self.keyValueObservation = KeyValueObservation() - + convenience init() { #if targetEnvironment(simulator) - self.fairPlaySessionManager = DefaultFairPlayStreamingSessionManager( - contentKeySession: AVContentKeySession(keySystem: .clearKey), - urlSession: .shared + self.init( + fairPlayStreamingSessionManager: DefaultFairPlayStreamingSessionManager( + contentKeySession: AVContentKeySession(keySystem: .clearKey), + urlSession: .shared + ) ) #else - self.fairPlaySessionManager = DefaultFairPlayStreamingSessionManager( - contentKeySession: AVContentKeySession(keySystem: .fairPlayStreaming), - urlSession: .shared + self.init( + fairPlayStreamingSessionManager: DefaultFairPlayStreamingSessionManager( + contentKeySession: AVContentKeySession(keySystem: .fairPlayStreaming), + urlSession: .shared + ) ) #endif } + init( + fairPlayStreamingSessionManager: FairPlayStreamingSessionManager + ) { + self.monitor = Monitor() + self.keyValueObservation = KeyValueObservation() + self.fairPlaySessionManager = fairPlayStreamingSessionManager + } + + func registerPlayerItem( + _ playerItem: AVPlayerItem, + playbackID: String, + playbackOptions: PlaybackOptions + ) { + // as? AVURLAsset check should never fail + if case .drm = playbackOptions.playbackPolicy, + let urlAsset = playerItem.asset as? AVURLAsset { + fairPlaySessionManager.registerPlaybackOptions( + playbackOptions, + for: playbackID + ) + // asset must be attached as early as possible to avoid crashes when attaching later + fairPlaySessionManager.addContentKeyRecipient( + urlAsset + ) + } + } + class KeyValueObservation { var observations: [ObjectIdentifier: NSKeyValueObservation] = [:] diff --git a/Sources/MuxPlayerSwift/PublicAPI/Extensions/AVPlayerItem+Mux.swift b/Sources/MuxPlayerSwift/PublicAPI/Extensions/AVPlayerItem+Mux.swift index 6c052c52..cfc5df53 100644 --- a/Sources/MuxPlayerSwift/PublicAPI/Extensions/AVPlayerItem+Mux.swift +++ b/Sources/MuxPlayerSwift/PublicAPI/Extensions/AVPlayerItem+Mux.swift @@ -96,19 +96,21 @@ fileprivate func makePlaybackURL( return playbackURL } -/// Create a new `AVAsset` that has been prepared for playback -/// If DRM is required, the Asset will be registered with the ``FairPlaySessionManager`` -fileprivate func makeAVAsset(playbackID: String, playbackOptions: PlaybackOptions) -> AVAsset { - let url = makePlaybackURL(playbackID: playbackID, playbackOptions: playbackOptions) - - let asset = AVURLAsset(url: url) - if case .drm(_) = playbackOptions.playbackPolicy { - PlayerSDK.shared.fairPlaySessionManager.registerPlaybackOptions(playbackOptions, for: playbackID) - // asset must be attached as early as possible to avoid crashes when attaching later - PlayerSDK.shared.fairPlaySessionManager.addContentKeyRecipient(asset) +// Create a new `AVAsset` that has been prepared for playback +internal extension AVURLAsset { + convenience init( + playbackID: String, + playbackOptions: PlaybackOptions + ) { + let url = makePlaybackURL( + playbackID: playbackID, + playbackOptions: playbackOptions + ) + + self.init( + url: url + ) } - - return asset } internal extension AVPlayerItem { @@ -124,12 +126,19 @@ internal extension AVPlayerItem { /// - Parameter playbackID: playback ID of the Mux Asset /// you'd like to play convenience init(playbackID: String) { + let playbackOptions = PlaybackOptions() let playbackURL = makePlaybackURL( playbackID: playbackID, - playbackOptions: PlaybackOptions() + playbackOptions: playbackOptions ) self.init(url: playbackURL) + + PlayerSDK.shared.registerPlayerItem( + self, + playbackID: playbackID, + playbackOptions: playbackOptions + ) } /// Initializes a player item with a playback URL that @@ -143,11 +152,17 @@ internal extension AVPlayerItem { playbackID: String, playbackOptions: PlaybackOptions ) { - let asset = makeAVAsset( + self.init( + asset: AVURLAsset( + playbackID: playbackID, + playbackOptions: playbackOptions + ) + ) + + PlayerSDK.shared.registerPlayerItem( + self, playbackID: playbackID, playbackOptions: playbackOptions ) - - self.init(asset: asset) } } From 553402d1919d78930eeba89832cdc911f8f3cfe3 Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 18:35:45 -0700 Subject: [PATCH 06/13] test: validate DRM registration when drm token provided --- .../FairPlaySessionManagerTests.swift | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift b/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift index 9ee863f3..69a0f585 100644 --- a/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift +++ b/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift @@ -563,4 +563,72 @@ class FairPlaySessionManagerTests : XCTestCase { return } } + + func testPlaybackOptionsRegistered() throws { + let mockURLSessionConfig = URLSessionConfiguration.default + mockURLSessionConfig.protocolClasses = [MockURLProtocol.self] + self.mockURLSession = URLSession.init(configuration: mockURLSessionConfig) + // .clearKey is used because .fairPlay requires a physical device + let session = AVContentKeySession( + keySystem: .clearKey + ) + let defaultFairPlaySessionManager = DefaultFairPlayStreamingSessionManager( + contentKeySession: session, + urlSession: mockURLSession + ) + self.sessionManager = defaultFairPlaySessionManager + let sessionDelegate = ContentKeySessionDelegate( + sessionManager: defaultFairPlaySessionManager + ) + defaultFairPlaySessionManager.sessionDelegate = sessionDelegate + + let fakeLicense = "fake-license-binary-data".data(using: .utf8) + let fakeAppCert = "fake-application-cert-binary-data".data(using: .utf8) + MockURLProtocol.requestHandler = { request in + + guard let url = request.url else { + fatalError() + } + + if (url.absoluteString.contains("appcert")) { + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: nil + )! + + return (response, fakeAppCert) + } else { + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: nil + )! + + + return (response, fakeLicense) + } + } + + #if DEBUG + PlayerSDK.shared = PlayerSDK( + fairPlayStreamingSessionManager: defaultFairPlaySessionManager + ) + #endif + + let i = AVPlayerItem( + playbackID: "abc", + playbackOptions: PlaybackOptions( + playbackToken: "def", + drmToken: "ghi" + ) + ) + + XCTAssertEqual( + defaultFairPlaySessionManager.playbackOptionsByPlaybackID.count, + 1 + ) + } } From 2e560f219b01d39e9a850d5f3832fbfe637c0881 Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 18:40:02 -0700 Subject: [PATCH 07/13] Non-public API, use extension to initialize URL --- .../InternalExtensions/AVPlayerItem+Mux.swift | 169 ++++++++++++++++++ .../Extensions/AVPlayerItem+Mux.swift | 168 ----------------- 2 files changed, 169 insertions(+), 168 deletions(-) create mode 100644 Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift delete mode 100644 Sources/MuxPlayerSwift/PublicAPI/Extensions/AVPlayerItem+Mux.swift diff --git a/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift b/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift new file mode 100644 index 00000000..787bedf3 --- /dev/null +++ b/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift @@ -0,0 +1,169 @@ +// +// AVPlayerItem+Mux.swift +// + +import AVFoundation +import Foundation + +internal extension URL { + static func make( + playbackID: String, + playbackOptions: PlaybackOptions + ) -> Self { + var components = URLComponents() + components.scheme = "https" + + if let customDomain = playbackOptions.customDomain { + components.host = "stream.\(customDomain)" + } else { + components.host = "stream.mux.com" + } + + components.path = "/\(playbackID).m3u8" + + if case PlaybackOptions.PlaybackPolicy.public(let publicPlaybackOptions) = playbackOptions.playbackPolicy { + var queryItems: [URLQueryItem] = [] + + if publicPlaybackOptions.useRedundantStreams { + queryItems.append( + URLQueryItem( + name: "redundant_streams", + value: "true" + ) + ) + } + + if publicPlaybackOptions.maximumResolutionTier != .default { + queryItems.append( + URLQueryItem( + name: "max_resolution", + value: publicPlaybackOptions.maximumResolutionTier.queryValue + ) + ) + } + + if publicPlaybackOptions.minimumResolutionTier != .default { + queryItems.append( + URLQueryItem( + name: "min_resolution", + value: publicPlaybackOptions.minimumResolutionTier.queryValue + ) + ) + } + + if publicPlaybackOptions.renditionOrder != .default { + queryItems.append( + URLQueryItem( + name: "rendition_order", + value: publicPlaybackOptions.renditionOrder.queryValue + ) + ) + } + + components.queryItems = queryItems + } else if case PlaybackOptions.PlaybackPolicy.signed(let signedPlaybackOptions) = playbackOptions.playbackPolicy { + + var queryItems: [URLQueryItem] = [] + + queryItems.append( + URLQueryItem( + name: "token", + value: signedPlaybackOptions.playbackToken + ) + ) + + components.queryItems = queryItems + + } else if case PlaybackOptions.PlaybackPolicy.drm(let drmPlaybackOptions) = playbackOptions.playbackPolicy { + + var queryItems: [URLQueryItem] = [] + + queryItems.append( + URLQueryItem( + name: "token", + value: drmPlaybackOptions.playbackToken + ) + ) + + components.queryItems = queryItems + + } + + guard let playbackURL = components.url else { + preconditionFailure("Invalid playback URL components") + } + + return playbackURL + } +} + +// Create a new `AVAsset` that has been prepared for playback +internal extension AVURLAsset { + convenience init( + playbackID: String, + playbackOptions: PlaybackOptions + ) { + let url = URL.make( + playbackID: playbackID, + playbackOptions: playbackOptions + ) + + self.init( + url: url + ) + } +} + +internal extension AVPlayerItem { + + /// Initializes a player item with a playback URL that + /// references your Mux Video at the supplied playback ID. + /// The playback ID must be public. + /// + /// This initializer uses https://stream.mux.com as the + /// base URL. Use a different initializer if using a custom + /// playback URL. + /// + /// - Parameter playbackID: playback ID of the Mux Asset + /// you'd like to play + convenience init(playbackID: String) { + let playbackOptions = PlaybackOptions() + let playbackURL = URL.make( + playbackID: playbackID, + playbackOptions: playbackOptions + ) + + self.init(url: playbackURL) + + PlayerSDK.shared.registerPlayerItem( + self, + playbackID: playbackID, + playbackOptions: playbackOptions + ) + } + + /// Initializes a player item with a playback URL that + /// references your Mux Video at the supplied playback ID. + /// The playback ID must be public. + /// + /// - Parameters: + /// - playbackID: playback ID of the Mux Asset + /// you'd like to play + convenience init( + playbackID: String, + playbackOptions: PlaybackOptions + ) { + self.init( + asset: AVURLAsset( + playbackID: playbackID, + playbackOptions: playbackOptions + ) + ) + + PlayerSDK.shared.registerPlayerItem( + self, + playbackID: playbackID, + playbackOptions: playbackOptions + ) + } +} diff --git a/Sources/MuxPlayerSwift/PublicAPI/Extensions/AVPlayerItem+Mux.swift b/Sources/MuxPlayerSwift/PublicAPI/Extensions/AVPlayerItem+Mux.swift deleted file mode 100644 index cfc5df53..00000000 --- a/Sources/MuxPlayerSwift/PublicAPI/Extensions/AVPlayerItem+Mux.swift +++ /dev/null @@ -1,168 +0,0 @@ -// -// AVPlayerItem+Mux.swift -// - -import AVFoundation -import Foundation - -fileprivate func makePlaybackURL( - playbackID: String, - playbackOptions: PlaybackOptions -) -> URL { - - var components = URLComponents() - components.scheme = "https" - - if let customDomain = playbackOptions.customDomain { - components.host = "stream.\(customDomain)" - } else { - components.host = "stream.mux.com" - } - - components.path = "/\(playbackID).m3u8" - - if case PlaybackOptions.PlaybackPolicy.public(let publicPlaybackOptions) = playbackOptions.playbackPolicy { - var queryItems: [URLQueryItem] = [] - - if publicPlaybackOptions.useRedundantStreams { - queryItems.append( - URLQueryItem( - name: "redundant_streams", - value: "true" - ) - ) - } - - if publicPlaybackOptions.maximumResolutionTier != .default { - queryItems.append( - URLQueryItem( - name: "max_resolution", - value: publicPlaybackOptions.maximumResolutionTier.queryValue - ) - ) - } - - if publicPlaybackOptions.minimumResolutionTier != .default { - queryItems.append( - URLQueryItem( - name: "min_resolution", - value: publicPlaybackOptions.minimumResolutionTier.queryValue - ) - ) - } - - if publicPlaybackOptions.renditionOrder != .default { - queryItems.append( - URLQueryItem( - name: "rendition_order", - value: publicPlaybackOptions.renditionOrder.queryValue - ) - ) - } - - components.queryItems = queryItems - } else if case PlaybackOptions.PlaybackPolicy.signed(let signedPlaybackOptions) = playbackOptions.playbackPolicy { - - var queryItems: [URLQueryItem] = [] - - queryItems.append( - URLQueryItem( - name: "token", - value: signedPlaybackOptions.playbackToken - ) - ) - - components.queryItems = queryItems - - } else if case PlaybackOptions.PlaybackPolicy.drm(let drmPlaybackOptions) = playbackOptions.playbackPolicy { - - var queryItems: [URLQueryItem] = [] - - queryItems.append( - URLQueryItem( - name: "token", - value: drmPlaybackOptions.playbackToken - ) - ) - - components.queryItems = queryItems - - } - - guard let playbackURL = components.url else { - preconditionFailure("Invalid playback URL components") - } - - return playbackURL -} - -// Create a new `AVAsset` that has been prepared for playback -internal extension AVURLAsset { - convenience init( - playbackID: String, - playbackOptions: PlaybackOptions - ) { - let url = makePlaybackURL( - playbackID: playbackID, - playbackOptions: playbackOptions - ) - - self.init( - url: url - ) - } -} - -internal extension AVPlayerItem { - - /// Initializes a player item with a playback URL that - /// references your Mux Video at the supplied playback ID. - /// The playback ID must be public. - /// - /// This initializer uses https://stream.mux.com as the - /// base URL. Use a different initializer if using a custom - /// playback URL. - /// - /// - Parameter playbackID: playback ID of the Mux Asset - /// you'd like to play - convenience init(playbackID: String) { - let playbackOptions = PlaybackOptions() - let playbackURL = makePlaybackURL( - playbackID: playbackID, - playbackOptions: playbackOptions - ) - - self.init(url: playbackURL) - - PlayerSDK.shared.registerPlayerItem( - self, - playbackID: playbackID, - playbackOptions: playbackOptions - ) - } - - /// Initializes a player item with a playback URL that - /// references your Mux Video at the supplied playback ID. - /// The playback ID must be public. - /// - /// - Parameters: - /// - playbackID: playback ID of the Mux Asset - /// you'd like to play - convenience init( - playbackID: String, - playbackOptions: PlaybackOptions - ) { - self.init( - asset: AVURLAsset( - playbackID: playbackID, - playbackOptions: playbackOptions - ) - ) - - PlayerSDK.shared.registerPlayerItem( - self, - playbackID: playbackID, - playbackOptions: playbackOptions - ) - } -} From af5c3be187e2e4ba8193346a529ce059566eda7b Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 18:55:45 -0700 Subject: [PATCH 08/13] avoid tripping up docc for internal extensions --- .../InternalExtensions/AVPlayerItem+Mux.swift | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift b/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift index 787bedf3..631190ef 100644 --- a/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift +++ b/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift @@ -116,16 +116,16 @@ internal extension AVURLAsset { internal extension AVPlayerItem { - /// Initializes a player item with a playback URL that - /// references your Mux Video at the supplied playback ID. - /// The playback ID must be public. - /// - /// This initializer uses https://stream.mux.com as the - /// base URL. Use a different initializer if using a custom - /// playback URL. - /// - /// - Parameter playbackID: playback ID of the Mux Asset - /// you'd like to play + // Initializes a player item with a playback URL that + // references your Mux Video at the supplied playback ID. + // The playback ID must be public. + // + // This initializer uses https://stream.mux.com as the + // base URL. Use a different initializer if using a custom + // playback URL. + // + // - Parameter playbackID: playback ID of the Mux Asset + // you'd like to play convenience init(playbackID: String) { let playbackOptions = PlaybackOptions() let playbackURL = URL.make( @@ -142,13 +142,13 @@ internal extension AVPlayerItem { ) } - /// Initializes a player item with a playback URL that - /// references your Mux Video at the supplied playback ID. - /// The playback ID must be public. - /// - /// - Parameters: - /// - playbackID: playback ID of the Mux Asset - /// you'd like to play + // Initializes a player item with a playback URL that + // references your Mux Video at the supplied playback ID. + // The playback ID must be public. + // + // - Parameters: + // - playbackID: playback ID of the Mux Asset + // you'd like to play convenience init( playbackID: String, playbackOptions: PlaybackOptions From f18c24fc6933df9e3f6fd6c2cf4cd7f09e74073a Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 19:04:11 -0700 Subject: [PATCH 09/13] cleaner test --- .../FairPlay/FairPlaySessionManagerTests.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift b/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift index 69a0f585..184e3111 100644 --- a/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift +++ b/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift @@ -565,6 +565,8 @@ class FairPlaySessionManagerTests : XCTestCase { } func testPlaybackOptionsRegistered() throws { + + #if DEBUG let mockURLSessionConfig = URLSessionConfiguration.default mockURLSessionConfig.protocolClasses = [MockURLProtocol.self] self.mockURLSession = URLSession.init(configuration: mockURLSessionConfig) @@ -612,11 +614,10 @@ class FairPlaySessionManagerTests : XCTestCase { } } - #if DEBUG + PlayerSDK.shared = PlayerSDK( fairPlayStreamingSessionManager: defaultFairPlaySessionManager ) - #endif let i = AVPlayerItem( playbackID: "abc", @@ -630,5 +631,11 @@ class FairPlaySessionManagerTests : XCTestCase { defaultFairPlaySessionManager.playbackOptionsByPlaybackID.count, 1 ) + #else + XCTExpectFailure( + "This test can only be run under a debug build configuration" + ) + XCTAssert(false) + #endif } } From 96a29e42af01013c71d6e46e6803407ddadaa810 Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 19:16:52 -0700 Subject: [PATCH 10/13] route calls to one chokepoint for simplicity --- .../InternalExtensions/AVPlayerItem+Mux.swift | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift b/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift index 631190ef..0373173c 100644 --- a/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift +++ b/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift @@ -97,22 +97,6 @@ internal extension URL { } } -// Create a new `AVAsset` that has been prepared for playback -internal extension AVURLAsset { - convenience init( - playbackID: String, - playbackOptions: PlaybackOptions - ) { - let url = URL.make( - playbackID: playbackID, - playbackOptions: playbackOptions - ) - - self.init( - url: url - ) - } -} internal extension AVPlayerItem { @@ -127,18 +111,9 @@ internal extension AVPlayerItem { // - Parameter playbackID: playback ID of the Mux Asset // you'd like to play convenience init(playbackID: String) { - let playbackOptions = PlaybackOptions() - let playbackURL = URL.make( - playbackID: playbackID, - playbackOptions: playbackOptions - ) - - self.init(url: playbackURL) - - PlayerSDK.shared.registerPlayerItem( - self, + self.init( playbackID: playbackID, - playbackOptions: playbackOptions + playbackOptions: PlaybackOptions() ) } @@ -153,13 +128,20 @@ internal extension AVPlayerItem { playbackID: String, playbackOptions: PlaybackOptions ) { - self.init( - asset: AVURLAsset( + + // Create a new `AVAsset` that has been prepared + // for playback + let asset = AVURLAsset( + url: URL.make( playbackID: playbackID, playbackOptions: playbackOptions ) ) + self.init( + asset: asset + ) + PlayerSDK.shared.registerPlayerItem( self, playbackID: playbackID, From c12d8b2ac4c3d8321988a5d703d8bfd4ef37f71e Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 20:06:57 -0700 Subject: [PATCH 11/13] We can use generics instead I think --- .../TestUtils/DummyAVContentKeySession.swift | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 Tests/MuxPlayerSwift/TestUtils/DummyAVContentKeySession.swift diff --git a/Tests/MuxPlayerSwift/TestUtils/DummyAVContentKeySession.swift b/Tests/MuxPlayerSwift/TestUtils/DummyAVContentKeySession.swift deleted file mode 100644 index e676a12f..00000000 --- a/Tests/MuxPlayerSwift/TestUtils/DummyAVContentKeySession.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// MockAVContentKeySession.swift -// -// -// Created by Emily Dixon on 5/2/24. -// - -import Foundation -import AVKit - -/// Dummy AVContentKeySession that does nothing -/// Warning! Only methods touched during tests are mocked. Be careful of false negatives! -class DummyAVContentKeySession: AVContentKeySession { - - - override func addContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { - } - - override func removeContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { - } -} From 6fe1bf6063b25a6749527d0392466a5150a37487 Mon Sep 17 00:00:00 2001 From: AJ Lauer Barinov Date: Fri, 3 May 2024 20:07:18 -0700 Subject: [PATCH 12/13] add a few comments --- .../FairPlay/FairPlaySessionManager.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift index 8e9ba3c5..93d46efa 100644 --- a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift +++ b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift @@ -10,6 +10,9 @@ import AVFoundation // 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 @@ -32,6 +35,8 @@ protocol FairPlayStreamingSessionCredentialClient: AnyObject { ) } +// MARK: - PlaybackOptionsRegistry + protocol PlaybackOptionsRegistry { /// Registers a ``PlaybackOptions`` for DRM playback, associated with the given playbackID func registerPlaybackOptions(_ opts: PlaybackOptions, for playbackID: String) @@ -41,7 +46,9 @@ protocol PlaybackOptionsRegistry { func unregisterPlaybackOptions(for playbackID: String) } -// MARK: registering drm-protected assets +// 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 @@ -50,6 +57,8 @@ protocol ContentKeyRecipientRegistry { func removeContentKeyRecipient(_ recipient: AVContentKeyRecipient) } +// MARK: - FairPlayStreamingSessionManager + typealias FairPlayStreamingSessionManager = FairPlayStreamingSessionCredentialClient & PlaybackOptionsRegistry & ContentKeyRecipientRegistry // MARK: - Content Key Provider From 18877d0d462f2f71be27b49372d6dc75376bf52f Mon Sep 17 00:00:00 2001 From: Emily Dixon Date: Mon, 6 May 2024 17:24:09 -0700 Subject: [PATCH 13/13] Need to set ContentKeyDelegate --- .../MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift index 4c44b8f5..9d4ccb58 100644 --- a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift +++ b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift @@ -29,12 +29,14 @@ class PlayerSDK { ) ) #else - self.init( - fairPlayStreamingSessionManager: DefaultFairPlayStreamingSessionManager( - contentKeySession: AVContentKeySession(keySystem: .fairPlayStreaming), - urlSession: .shared - ) + let sessionManager = DefaultFairPlayStreamingSessionManager( + contentKeySession: AVContentKeySession(keySystem: .fairPlayStreaming), + urlSession: .shared + ) + sessionManager.sessionDelegate = ContentKeySessionDelegate( + sessionManager: sessionManager ) + self.init(fairPlayStreamingSessionManager: sessionManager) #endif }