From 2754343e9a7561cdf3575ba96a5b2ec54d9b7591 Mon Sep 17 00:00:00 2001 From: Stefano Russello Date: Mon, 17 Jun 2024 14:06:54 +0200 Subject: [PATCH 1/3] ZEUS-4232 Background audio - Added background playback enabled by default on playback config (needs UIBackgroundModes on demo app) - Setup with video player config --- .../BitMovinPlugin/BitmovinPlayerPlugin.swift | 7 ++++--- .../PlaybackSDK/Player Plugin/VideoPlayerConfig.swift | 10 ++++++++++ .../PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 Sources/PlaybackSDK/Player Plugin/VideoPlayerConfig.swift diff --git a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift index c87909d..29c404e 100644 --- a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift +++ b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift @@ -21,6 +21,7 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin { let playerConfig = PlayerConfig() playerConfig.playbackConfig.isAutoplayEnabled = true + playerConfig.playbackConfig.isBackgroundPlaybackEnabled = true playerConfig.key = PlayBackSDKManager.shared.bitmovinLicense self.playerConfig = playerConfig @@ -33,9 +34,9 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin { } // MARK: VideoPlayerPlugin protocol implementation - public func setup() { - // Additional setup logic if needed, not required in this case - // Might call here the configuration API + public func setup(config: VideoPlayerConfig) { + playerConfig.playbackConfig.isAutoplayEnabled = config.playbackConfig.autoplayEnabled + playerConfig.playbackConfig.isBackgroundPlaybackEnabled = config.playbackConfig.backgroundPlaybackEnabled } public func playerView(hlsURLString: String) -> AnyView { diff --git a/Sources/PlaybackSDK/Player Plugin/VideoPlayerConfig.swift b/Sources/PlaybackSDK/Player Plugin/VideoPlayerConfig.swift new file mode 100644 index 0000000..6138e97 --- /dev/null +++ b/Sources/PlaybackSDK/Player Plugin/VideoPlayerConfig.swift @@ -0,0 +1,10 @@ +public struct VideoPlayerConfig { + public var playbackConfig = PlaybackConfig() + + public init() {} +} + +public class PlaybackConfig { + public var autoplayEnabled: Bool = true + public var backgroundPlaybackEnabled: Bool = true +} diff --git a/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift b/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift index a332c0c..7240dee 100644 --- a/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift +++ b/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift @@ -14,7 +14,7 @@ public protocol VideoPlayerPlugin: AnyObject { var name: String { get } var version: String { get } - func setup() + func setup(config: VideoPlayerConfig) // TODO: add event /// func handleEvent(event: BitmovinPlayerCore.PlayerEvent) From b6dca1938bd6eba62bb2b7363b7ee27a01c96b31 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Mon, 17 Jun 2024 17:38:20 +0300 Subject: [PATCH 2/3] [ZEUS-4329] Adde enum for Playback error reasons --- .../PlayBack API/PlayBackAPIService.swift | 8 +-- Sources/PlaybackSDK/PlayBackSDKManager.swift | 49 ++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/Sources/PlaybackSDK/PlayBack API/PlayBackAPIService.swift b/Sources/PlaybackSDK/PlayBack API/PlayBackAPIService.swift index 43bc68e..0fa7ac7 100644 --- a/Sources/PlaybackSDK/PlayBack API/PlayBackAPIService.swift +++ b/Sources/PlaybackSDK/PlayBack API/PlayBackAPIService.swift @@ -67,10 +67,12 @@ internal class PlayBackAPIService: PlayBackAPI { return data default: let decoder = JSONDecoder() - if let errorResponse = try? decoder.decode(PlaybackResponseModel.self, from: data) { - throw PlayBackAPIError.apiError(statusCode: httpResponse.statusCode, message: errorResponse.message ?? "Unknown authentication error message", reason: errorResponse.reason ?? "Unknown authentication error reason") + if let errorResponse = try? decoder.decode(PlaybackResponseModel.self, from: data) { + let errorReason = errorResponse.reason ?? "Unknown authentication error reason" + throw PlayBackAPIError.apiError(statusCode: httpResponse.statusCode, message: errorResponse.message ?? "Unknown authentication error message", reason: PlaybackErrorReason(fromString: errorReason)) } else { - throw PlayBackAPIError.apiError(statusCode: httpResponse.statusCode, message: "Unknown authentication error", reason: "Unknown authentication error reason") + let errorReason = "Unknown authentication error reason" + throw PlayBackAPIError.apiError(statusCode: httpResponse.statusCode, message: "Unknown authentication error", reason: PlaybackErrorReason(fromString: errorReason)) } } } diff --git a/Sources/PlaybackSDK/PlayBackSDKManager.swift b/Sources/PlaybackSDK/PlayBackSDKManager.swift index c105adb..1642ebb 100644 --- a/Sources/PlaybackSDK/PlayBackSDKManager.swift +++ b/Sources/PlaybackSDK/PlayBackSDKManager.swift @@ -18,6 +18,53 @@ public enum SDKError: Error { case loadHLSStreamError } +// Define reason codes returned by Playback SDK +public enum PlaybackErrorReason: Equatable { + // Http error 400 + case headerError + case badRequestError + case siteNotFound + case configurationError + case apiKeyError + case mpPartnerError + + // Http error 401 + case tokenError + case tooManyDevices + case tooManyRequests + case noEntitlement + case noSubscription + case noActiveSession + case notAuthenticated + + // Http error 404 + case noEntityExist + + // Unknown error with associated custom message + case unknownError(String) + + init(fromString value: String) { + switch value.uppercased() { + case "HEADER_ERROR": self = .headerError + case "BAD_REQUEST_ERROR": self = .badRequestError + case "SITE_NOT_FOUND": self = .siteNotFound + case "CONFIGURATION_ERROR": self = .configurationError + case "API_KEY_ERROR": self = .apiKeyError + case "MP_PARTNER_ERROR": self = .mpPartnerError + case "TOKEN_ERROR": self = .tokenError + case "TOO_MANY_DEVICES": self = .tooManyDevices + case "TOO_MANY_REQUESTS": self = .tooManyRequests + case "NO_ENTITLEMENT": self = .noEntitlement + case "NO_SUBSCRIPTION": self = .noSubscription + case "NO_ACTIVE_SESSION": self = .noActiveSession + case "NOT_AUTHENTICATED": self = .notAuthenticated + case "NO_ENTITY_EXIST": self = .noEntityExist + default: self = .unknownError(value) + } + } +} + + /** Define the errors that can occur during API interactions */ @@ -30,7 +77,7 @@ public enum PlayBackAPIError: Error { case loadHLSStreamError case networkError(Error) - case apiError(statusCode: Int, message: String, reason: String) + case apiError(statusCode: Int, message: String, reason: PlaybackErrorReason) } From 85637ea8336b868e4c439645bcebb95ec33646af Mon Sep 17 00:00:00 2001 From: Stefano Russello Date: Thu, 20 Jun 2024 14:01:28 +0200 Subject: [PATCH 3/3] ZEUS-4232 Background audio - Fixed PlaybackConfig from class to struct ( thanks to @artem-y-pamediagroup ) - Manage background audio with iOS Remote Command Center - Added mediaTitle for iOS Remote Command Center - Unload on Remove player function - Audio session for background playback - Manage play / pause buttons from RemoteCommandCenter - Remove Remote Transport Controls and Audio Session --- Sources/PlaybackSDK/PlayBackSDKManager.swift | 4 +- .../BitMovinPlugin/BitMovinPlayerView.swift | 95 ++++++++++++++++++- .../BitMovinPlugin/BitmovinPlayerPlugin.swift | 5 +- .../Player Plugin/VideoPlayerConfig.swift | 2 +- .../Player Plugin/VideoPlayerPlugin.swift | 2 +- .../PlayerUIView/PlaybackUIView.swift | 8 +- 6 files changed, 106 insertions(+), 10 deletions(-) diff --git a/Sources/PlaybackSDK/PlayBackSDKManager.swift b/Sources/PlaybackSDK/PlayBackSDKManager.swift index c105adb..70f3d54 100644 --- a/Sources/PlaybackSDK/PlayBackSDKManager.swift +++ b/Sources/PlaybackSDK/PlayBackSDKManager.swift @@ -99,8 +99,8 @@ public class PlayBackSDKManager { ```swift let playerView = loadPlayer(entryID: "exampleEntryID", authorizationToken: "exampleToken") */ - public func loadPlayer(entryID: String, authorizationToken: String? = nil, onError: ((PlayBackAPIError) -> Void)?) -> some View { - return PlaybackUIView(entryId: entryID, authorizationToken: authorizationToken, onError: onError) + public func loadPlayer(entryID: String, authorizationToken: String? = nil, mediaTitle: String? = nil, onError: ((PlayBackAPIError) -> Void)?) -> some View { + return PlaybackUIView(entryId: entryID, authorizationToken: authorizationToken, mediaTitle: mediaTitle, onError: onError) } // MARK: Private fuctions diff --git a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitMovinPlayerView.swift b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitMovinPlayerView.swift index b6db0da..684c5f6 100644 --- a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitMovinPlayerView.swift +++ b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitMovinPlayerView.swift @@ -7,6 +7,7 @@ #if !os(macOS) import SwiftUI import BitmovinPlayer +import MediaPlayer public struct BitMovinPlayerView: View { internal let player: Player @@ -17,10 +18,12 @@ public struct BitMovinPlayerView: View { guard let hlsURL = URL(string: hlsURLString) else { return nil } - return SourceConfig(url: hlsURL, type: .hls) + let sConfig = SourceConfig(url: hlsURL, type: .hls) + + return sConfig } - public init(hlsURLString: String, playerConfig: PlayerConfig) { + public init(hlsURLString: String, playerConfig: PlayerConfig, title: String) { self.hlsURLString = hlsURLString @@ -35,6 +38,15 @@ public struct BitMovinPlayerView: View { // Create player view configuration self.playerViewConfig = PlayerViewConfig() + + // Setup remote control commands to be able to control playback from Control Center + setupRemoteTransportControls() + + // Set playback metadata. Updates to the other metadata values are done in the specific listeners + setupNowPlayingMetadata(key: MPMediaItemPropertyTitle, value: title) + + // Make sure that the correct audio session category is set to allow for background playback. + handleAudioSessionCategorySetting() } @@ -59,6 +71,85 @@ public struct BitMovinPlayerView: View { player.load(sourceConfig: sourceConfig) } } + .onDisappear { + removeRemoteTransportControlsAndAudioSession() + } + } + + func setupRemoteTransportControls() { + // Get the shared MPRemoteCommandCenter + let commandCenter = MPRemoteCommandCenter.shared() + + // Add handler for Play Command + commandCenter.playCommand.addTarget(handler: playTarget) + commandCenter.playCommand.isEnabled = true + + // Add handler for Pause Command + commandCenter.pauseCommand.addTarget(handler: pauseTarget) + commandCenter.pauseCommand.isEnabled = true } + + /// Remove RemoteCommandCenter and AudioSession + func removeRemoteTransportControlsAndAudioSession() { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = false + commandCenter.playCommand.removeTarget(playTarget) + commandCenter.pauseCommand.isEnabled = false + commandCenter.pauseCommand.removeTarget(pauseTarget) + + let sessionAV = AVAudioSession.sharedInstance() + try? sessionAV.setActive(false, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation) + MPNowPlayingInfoCenter.default().nowPlayingInfo = [:] + UIApplication.shared.endReceivingRemoteControlEvents() + } + /// Play Target for RemoteCommandCenter + func playTarget(_: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + guard let player = self.player as? Player else { return .commandFailed } + + player.play() + if player.isPlaying { + return .success + } + return .commandFailed + } + + /// Pause Target for RemoteCommandCenter + func pauseTarget(_: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + guard let player = self.player as? Player else { return .commandFailed } + + player.pause() + if player.isPaused { + return .success + } + return .commandFailed + } + + func setupNowPlayingMetadata(key: String, value: Any) { + var nowPlayingInfo: [String: Any] = [:] + nowPlayingInfo[key] = value + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + UIApplication.shared.beginReceivingRemoteControlEvents() + } + + /* Set AVAudioSessionCategoryPlayback category on the audio session. This category indicates that audio playback + is a central feature of your app. When you specify this category, your app’s audio continues with the Ring/Silent + switch set to silent mode (iOS only). With this category, your app can also play background audio if you're + using the Audio, AirPlay, and Picture in Picture background mode. To enable this mode, under the Capabilities + tab in your XCode project, set the Background Modes switch to ON and select the “Audio, AirPlay, and Picture in + Picture” option under the list of available modes. */ + func handleAudioSessionCategorySetting() { + let audioSession = AVAudioSession.sharedInstance() + + // When AVAudioSessionCategoryPlayback is already active, we have nothing to do here + guard audioSession.category.rawValue != AVAudioSession.Category.playback.rawValue else { return } + + do { + try audioSession.setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode.moviePlayback) + try audioSession.setActive(true) + } catch { + print("Setting category to AVAudioSessionCategoryPlayback failed.") + } + } + } #endif diff --git a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift index 29c404e..a362464 100644 --- a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift +++ b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift @@ -39,8 +39,8 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin { playerConfig.playbackConfig.isBackgroundPlaybackEnabled = config.playbackConfig.backgroundPlaybackEnabled } - public func playerView(hlsURLString: String) -> AnyView { - let videoPlayerView = BitMovinPlayerView(hlsURLString: hlsURLString, playerConfig: playerConfig) + public func playerView(hlsURLString: String, title: String = "") -> AnyView { + let videoPlayerView = BitMovinPlayerView(hlsURLString: hlsURLString, playerConfig: playerConfig, title: title) self.player = videoPlayerView @@ -56,6 +56,7 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin { } public func removePlayer() { + player?.player.unload() player?.player.destroy() player = nil } diff --git a/Sources/PlaybackSDK/Player Plugin/VideoPlayerConfig.swift b/Sources/PlaybackSDK/Player Plugin/VideoPlayerConfig.swift index 6138e97..34cba3b 100644 --- a/Sources/PlaybackSDK/Player Plugin/VideoPlayerConfig.swift +++ b/Sources/PlaybackSDK/Player Plugin/VideoPlayerConfig.swift @@ -4,7 +4,7 @@ public struct VideoPlayerConfig { public init() {} } -public class PlaybackConfig { +public struct PlaybackConfig { public var autoplayEnabled: Bool = true public var backgroundPlaybackEnabled: Bool = true } diff --git a/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift b/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift index 7240dee..aee1388 100644 --- a/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift +++ b/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift @@ -19,7 +19,7 @@ public protocol VideoPlayerPlugin: AnyObject { // TODO: add event /// func handleEvent(event: BitmovinPlayerCore.PlayerEvent) - func playerView(hlsURLString: String) -> AnyView + func playerView(hlsURLString: String, title: String) -> AnyView func play() diff --git a/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift b/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift index a0eb13a..cde80d3 100644 --- a/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift +++ b/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift @@ -16,6 +16,9 @@ internal struct PlaybackUIView: View { /// The entry ID of the video to be played. private var entryId: String + /// The title of the video to be played. + private var mediaTitle: String? + /// Optional authorization token if required to fetch the video details. private var authorizationToken: String? @@ -37,10 +40,11 @@ internal struct PlaybackUIView: View { - entryId: The entry ID of the video to be played. - authorizationToken: Optional authorization token if required to fetch the video details. */ - internal init(entryId: String, authorizationToken: String?, onError: ((PlayBackAPIError) -> Void)?) { + internal init(entryId: String, authorizationToken: String?, mediaTitle: String?, onError: ((PlayBackAPIError) -> Void)?) { self.entryId = entryId self.authorizationToken = authorizationToken self.onError = onError + self.mediaTitle = mediaTitle } /// The body of the view. @@ -54,7 +58,7 @@ internal struct PlaybackUIView: View { } else { if let videoURL = videoURL { if let plugin = pluginManager.selectedPlugin { - plugin.playerView(hlsURLString: videoURL.absoluteString) + plugin.playerView(hlsURLString: videoURL.absoluteString, title: self.mediaTitle ?? "") } else { ErrorUIView(errorMessage: "No plugin selected") .background(Color.white)