diff --git a/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift b/Sources/MuxPlayerSwift/FairPlay/ContentKeySessionDelegate.swift index 812db100..2a5a6f7d 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..93d46efa 100644 --- a/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift +++ b/Sources/MuxPlayerSwift/FairPlay/FairPlaySessionManager.swift @@ -8,19 +8,23 @@ import Foundation import AVFoundation -protocol FairPlaySessionManager { - +// MARK: - FairPlayStreamingSessionManager + +// Use AnyObject to restrict conformances only to reference +// types because the SDKs AVContentKeySessionDelegate holds +// a weak reference to the SDKs witness of this. +protocol FairPlayStreamingSessionCredentialClient: AnyObject { // MARK: Requesting licenses and certs - - /// Requests the App Certificate for a playback id + + // Requests the App Certificate for a playback id func requestCertificate( fromDomain rootDomain: String, playbackID: String, drmToken: String, completion requestCompletion: @escaping (Result) -> 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, @@ -29,14 +33,11 @@ protocol FairPlaySessionManager { 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) +} + +// MARK: - PlaybackOptionsRegistry + +protocol PlaybackOptionsRegistry { /// Registers a ``PlaybackOptions`` for DRM playback, associated with the given playbackID func registerPlaybackOptions(_ opts: PlaybackOptions, for playbackID: String) /// Gets a DRM token previously registered via ``registerPlaybackOptions`` @@ -45,16 +46,49 @@ protocol FairPlaySessionManager { func unregisterPlaybackOptions(for playbackID: String) } +// MARK: - ContentKeyRecipientRegistry + +// Intended for registering drm-protected AVURLAssets +protocol ContentKeyRecipientRegistry { + /// Adds a ``AVContentKeyRecipient`` (probably an ``AVURLAsset``) that must be played + /// with DRM protection. This call is necessary for DRM playback to succeed + func addContentKeyRecipient(_ recipient: AVContentKeyRecipient) + /// Removes a ``AVContentKeyRecipient`` previously added by ``addContentKeyRecipient`` + func removeContentKeyRecipient(_ recipient: AVContentKeyRecipient) +} + +// MARK: - FairPlayStreamingSessionManager + +typealias FairPlayStreamingSessionManager = FairPlayStreamingSessionCredentialClient & PlaybackOptionsRegistry & ContentKeyRecipientRegistry + +// MARK: - Content Key Provider + +// Define protocol for calls made to AVContentKeySession +protocol ContentKeyProvider { + func setDelegate( + _ delegate: (any AVContentKeySessionDelegate)?, + queue delegateQueue: dispatch_queue_t? + ) + + func addContentKeyRecipient(_ recipient: any AVContentKeyRecipient) + + func removeContentKeyRecipient(_ recipient: any AVContentKeyRecipient) +} + +// AVContentKeySession already has built-in definitions for +// these methods so this declaration can be empty +extension AVContentKeySession: ContentKeyProvider { } + // MARK: helpers for interacting with the license server -extension DefaultFPSSManager { - /// Generates a domain name appropriate for the Mux license proxy associted with the given - /// "root domain". For example `mux.com` returns `license.mux.com` and - /// `customdomain.xyz.com` returns `license.customdomain.xyz.com` - static func makeLicenseDomain(_ rootDomain: String) -> String { +extension String { + // Generates a domain name appropriate for the Mux license proxy associted with the given + // "root domain". For example `mux.com` returns `license.mux.com` and + // `customdomain.xyz.com` returns `license.customdomain.xyz.com` + static func makeLicenseDomain(rootDomain: String) -> Self { let customDomainWithDefault = rootDomain let licenseDomain = "license.\(customDomainWithDefault)" - + // TODO: this check should not reach production or playing from staging will probably break if("staging.mux.com" == customDomainWithDefault) { return "license.gcp-us-west1-vos1.staging.mux.com" @@ -62,41 +96,63 @@ 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 { - - private var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:] +// MARK: - DefaultFairPlayStreamingSessionManager + +class DefaultFairPlayStreamingSessionManager< + ContentKeySession: ContentKeyProvider +>: FairPlayStreamingSessionManager { + + var playbackOptionsByPlaybackID: [String: PlaybackOptions] = [:] // note - null on simulators or other environments where fairplay isn't supported - private let contentKeySession: AVContentKeySession? - private let sessionDelegate: AVContentKeySessionDelegate? - + let contentKeySession: ContentKeySession + + var sessionDelegate: AVContentKeySessionDelegate? { + didSet { + contentKeySession.setDelegate( + sessionDelegate, + queue: DispatchQueue( + label: "com.mux.player.fairplay" + ) + ) + } + } + private let urlSession: URLSession func addContentKeyRecipient(_ recipient: AVContentKeyRecipient) { - contentKeySession?.addContentKeyRecipient(recipient) + contentKeySession.addContentKeyRecipient(recipient) } func removeContentKeyRecipient(_ recipient: AVContentKeyRecipient) { - contentKeySession?.removeContentKeyRecipient(recipient) + contentKeySession.removeContentKeyRecipient(recipient) } // MARK: Requesting licenses and certs @@ -108,10 +164,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 +186,7 @@ class DefaultFPSSManager: FairPlaySessionManager { let errorUtf = String(data: errorBody, encoding: .utf8) print("Cert Error: \(errorUtf ?? "nil")") } - + } // error case: I/O failed if let error = error { @@ -178,10 +236,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 +256,7 @@ class DefaultFPSSManager: FairPlaySessionManager { print("\t with header fields: \(String(describing: request.allHTTPHeaderFields))") let task = urlSession.dataTask(with: request) { [requestCompletion] data, response, error in - // error case: I/O failed + // error case: I/O failed if let error = error { print("URL Session Task Failed: \(error.localizedDescription)") requestCompletion(Result.failure( @@ -243,13 +303,18 @@ class DefaultFPSSManager: FairPlaySessionManager { // MARK: registering assets /// Registers a ``PlaybackOptions`` for DRM playback, associated with the given playbackID - func registerPlaybackOptions(_ opts: PlaybackOptions, for playbackID: String) { + func registerPlaybackOptions( + _ options: PlaybackOptions, + for playbackID: String + ) { print("Registering playbackID \(playbackID)") - playbackOptionsByPlaybackID[playbackID] = opts + playbackOptionsByPlaybackID[playbackID] = options } /// Gets a DRM token previously registered via ``registerPlaybackOptions`` - func findRegisteredPlaybackOptions(for playbackID: String) -> PlaybackOptions? { + func findRegisteredPlaybackOptions( + for playbackID: String + ) -> PlaybackOptions? { print("Finding playbackID \(playbackID)") return playbackOptionsByPlaybackID[playbackID] } @@ -259,42 +324,20 @@ class DefaultFPSSManager: FairPlaySessionManager { print("UN-Registering playbackID \(playbackID)") playbackOptionsByPlaybackID.removeValue(forKey: playbackID) } - - - + // MARK: initializers - - convenience init() { -#if targetEnvironment(simulator) - let session: AVContentKeySession? = nil - let delegate: AVContentKeySessionDelegate? = nil -#else - let session = AVContentKeySession(keySystem: .fairPlayStreaming) - let delegate = ContentKeySessionDelegate() -#endif - - self.init( - contentKeySession: session, - sessionDelegate: delegate, - sessionDelegateQueue: DispatchQueue(label: "com.mux.player.fairplay"), - urlSession: URLSession.shared - ) - } - + init( - contentKeySession: AVContentKeySession?, - sessionDelegate: AVContentKeySessionDelegate?, - sessionDelegateQueue: DispatchQueue, + contentKeySession: ContentKeySession, urlSession: URLSession ) { - contentKeySession?.setDelegate(sessionDelegate, queue: sessionDelegateQueue) - self.contentKeySession = contentKeySession - self.sessionDelegate = sessionDelegate self.urlSession = urlSession } } +// MARK: - FairPlaySessionError + enum FairPlaySessionError : Error { case because(cause: any Error) case httpFailed(responseStatusCode: Int) diff --git a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift index 79a8e793..9d4ccb58 100644 --- a/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift +++ b/Sources/MuxPlayerSwift/GlobalLifecycle/PlayerSDK.swift @@ -18,12 +18,53 @@ class PlayerSDK { let keyValueObservation: KeyValueObservation - let fairPlaySessionManager: FairPlaySessionManager + let fairPlaySessionManager: FairPlayStreamingSessionManager - init() { + convenience init() { + #if targetEnvironment(simulator) + self.init( + fairPlayStreamingSessionManager: DefaultFairPlayStreamingSessionManager( + contentKeySession: AVContentKeySession(keySystem: .clearKey), + urlSession: .shared + ) + ) + #else + let sessionManager = DefaultFairPlayStreamingSessionManager( + contentKeySession: AVContentKeySession(keySystem: .fairPlayStreaming), + urlSession: .shared + ) + sessionManager.sessionDelegate = ContentKeySessionDelegate( + sessionManager: sessionManager + ) + self.init(fairPlayStreamingSessionManager: sessionManager) + #endif + } + + init( + fairPlayStreamingSessionManager: FairPlayStreamingSessionManager + ) { self.monitor = Monitor() self.keyValueObservation = KeyValueObservation() - self.fairPlaySessionManager = DefaultFPSSManager() + 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 { diff --git a/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift b/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift new file mode 100644 index 00000000..0373173c --- /dev/null +++ b/Sources/MuxPlayerSwift/InternalExtensions/AVPlayerItem+Mux.swift @@ -0,0 +1,151 @@ +// +// 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 + } +} + + +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) { + self.init( + 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 + ) { + + // 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, + 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 6c052c52..00000000 --- a/Sources/MuxPlayerSwift/PublicAPI/Extensions/AVPlayerItem+Mux.swift +++ /dev/null @@ -1,153 +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 -/// 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) - } - - return asset -} - -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 playbackURL = makePlaybackURL( - playbackID: playbackID, - playbackOptions: PlaybackOptions() - ) - - self.init(url: playbackURL) - } - - /// 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 - ) { - let asset = makeAVAsset( - playbackID: playbackID, - playbackOptions: playbackOptions - ) - - self.init(asset: asset) - } -} diff --git a/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift b/Tests/MuxPlayerSwift/FairPlay/FairPlaySessionManagerTests.swift index 29eaf4f9..05ea9d97 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! + private var sessionManager: FairPlayStreamingSessionManager! 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 = DefaultFairPlayStreamingSessionManager( // .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)" @@ -557,4 +562,79 @@ class FairPlaySessionManagerTests : XCTestCase { return } } + + func testPlaybackOptionsRegistered() throws { + + #if DEBUG + 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) + } + } + + + PlayerSDK.shared = PlayerSDK( + fairPlayStreamingSessionManager: defaultFairPlaySessionManager + ) + + let i = AVPlayerItem( + playbackID: "abc", + playbackOptions: PlaybackOptions( + playbackToken: "def", + drmToken: "ghi" + ) + ) + + XCTAssertEqual( + defaultFairPlaySessionManager.playbackOptionsByPlaybackID.count, + 1 + ) + #else + XCTExpectFailure( + "This test can only be run under a debug build configuration" + ) + XCTAssert(false) + #endif + } } 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..322c0bf3 --- /dev/null +++ b/Tests/MuxPlayerSwift/Helpers/TestContentKeySession.swift @@ -0,0 +1,37 @@ +// +// TestContentKeySession.swift +// +// +// Created by Emily Dixon on 5/2/24. +// + +import Foundation +import AVKit + +@testable import MuxPlayerSwift + +class TestContentKeySession: ContentKeyProvider { + + var delegate: (any AVContentKeySessionDelegate)? + + var contentKeyRecipients: [any AVContentKeyRecipient] = [] + + func setDelegate( + _ delegate: (any AVContentKeySessionDelegate)?, + queue delegateQueue: dispatch_queue_t? + ) { + self.delegate = delegate + } + + func addContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { + contentKeyRecipients.append(recipient) + } + + func removeContentKeyRecipient(_ recipient: any AVContentKeyRecipient) { + // no-op + } + + init() { + + } +} 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) { - } -} diff --git a/Tests/MuxPlayerSwift/TestUtils/DummyAVContentKeySessionDelegate.swift b/Tests/MuxPlayerSwift/TestUtils/DummyAVContentKeySessionDelegate.swift deleted file mode 100644 index 6dcb1c09..00000000 --- a/Tests/MuxPlayerSwift/TestUtils/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 - } - -}