Skip to content

Commit

Permalink
Merge pull request #23 from StreamAMG/bugfix/ZEUS-4591-video-playing-…
Browse files Browse the repository at this point in the history
…in-background-when-switching

[ZEUS-4591] Fix video playing in background when switching
  • Loading branch information
artem-y-pamediagroup authored Jul 12, 2024
2 parents a5e43fa + f0a333b commit f7f7727
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 70 deletions.
16 changes: 14 additions & 2 deletions Sources/PlaybackSDK/PlayBackSDKManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,20 @@ public class PlayBackSDKManager {
```swift
let playerView = loadPlayer(entryID: "exampleEntryID", authorizationToken: "exampleToken")
*/
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)
public func loadPlayer(
entryID: String,
authorizationToken: String? = nil,
mediaTitle: String? = nil,
onError: ((PlayBackAPIError) -> Void)?
) -> some View {

PlaybackUIView(
entryId: entryID,
authorizationToken: authorizationToken,
mediaTitle: mediaTitle,
onError: onError
)
.id(entryID)
}

// MARK: Private fuctions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ import BitmovinPlayer
import MediaPlayer

public struct BitMovinPlayerView: View {
internal let player: Player
private let playerViewConfig: PlayerViewConfig
// These targets are used by the MPRemoteCommandCenter,
// to remove the command event handlers from memory.
@State private var playEventTarget: Any?
@State private var pauseEventTarget: Any?

private let player: Player
private let playerViewConfig = PlayerViewConfig()
private let hlsURLString: String

private var sourceConfig: SourceConfig? {
guard let hlsURL = URL(string: hlsURLString) else {
return nil
Expand All @@ -22,7 +27,34 @@ public struct BitMovinPlayerView: View {

return sConfig
}


/// Initializes the view with the player passed from outside.
///
/// This version of the initializer does not modify the `player`'s configuration, so any additional configuration steps
/// like setting `userInterfaceConfig` should be performed externally.
///
/// - parameter hlsURLString: Full URL of the HLS video stream that will be loaded by the player as the video source
/// - parameter player: Instance of the player that was created and configured outside of this view.
/// - parameter title: Video source title that will be set in playback metadata for the "now playing" source
public init(hlsURLString: String, player: Player, title: String) {

self.hlsURLString = hlsURLString

self.player = player

setup(title: title)
}

/// Initializes the view with an instance of player created inside of it, upon initialization.
///
/// In this version of the initializer, a `userInterfaceConfig` is being added to the `playerConfig`'s style configuration.
///
/// - Note: If the player config had `userInterfaceConfig` already modified before passing into this `init`,
/// those changes will take no effect sicne they will get overwritten here.
///
/// - parameter hlsURLString: Full URL of the HLS video stream that will be loaded by the player as the video source
/// - parameter playerConfig: Configuration that will be passed into the player upon creation, with an additional update in this initializer.
/// - parameter title: Video source title that will be set in playback metadata for the "now playing" source
public init(hlsURLString: String, playerConfig: PlayerConfig, title: String) {

self.hlsURLString = hlsURLString
Expand All @@ -35,21 +67,10 @@ public struct BitMovinPlayerView: View {
self.player = PlayerFactory.createPlayer(
playerConfig: playerConfig
)

// 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()

setup(title: title)
}



public var body: some View {
ZStack {
Color.black
Expand All @@ -65,7 +86,6 @@ public struct BitMovinPlayerView: View {
dump(event, name: "[Source Event]", maxDepth: 1)
}
}
// .padding()
.onAppear {
if let sourceConfig = self.sourceConfig {
player.load(sourceConfig: sourceConfig)
Expand All @@ -75,53 +95,47 @@ public struct BitMovinPlayerView: View {
removeRemoteTransportControlsAndAudioSession()
}
}

func setupRemoteTransportControls() {
// Get the shared MPRemoteCommandCenter
let commandCenter = MPRemoteCommandCenter.shared()

// Add handler for Play Command
commandCenter.playCommand.addTarget(handler: playTarget)
playEventTarget = commandCenter.playCommand.addTarget(handler: handleRemotePlayEvent)
commandCenter.playCommand.isEnabled = true

// Add handler for Pause Command
commandCenter.pauseCommand.addTarget(handler: pauseTarget)
pauseEventTarget = commandCenter.pauseCommand.addTarget(handler: handleRemotePauseEvent)
commandCenter.pauseCommand.isEnabled = true
}

/// Remove RemoteCommandCenter and AudioSession
func removeRemoteTransportControlsAndAudioSession() {
let commandCenter = MPRemoteCommandCenter.shared()

commandCenter.playCommand.isEnabled = false
commandCenter.playCommand.removeTarget(playTarget)
commandCenter.playCommand.removeTarget(playEventTarget)
playEventTarget = nil

commandCenter.pauseCommand.isEnabled = false
commandCenter.pauseCommand.removeTarget(pauseTarget)

commandCenter.pauseCommand.removeTarget(pauseEventTarget)
pauseEventTarget = nil

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 }

/// Play event handler for RemoteCommandCenter
func handleRemotePlayEvent(_: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
player.play()
if player.isPlaying {
return .success
}
return .commandFailed
return player.isPlaying ? .success : .commandFailed
}

/// Pause Target for RemoteCommandCenter
func pauseTarget(_: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
guard let player = self.player as? Player else { return .commandFailed }

/// Pause event handler for RemoteCommandCenter
func handleRemotePauseEvent(_: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
player.pause()
if player.isPaused {
return .success
}
return .commandFailed
return player.isPaused ? .success : .commandFailed
}

func setupNowPlayingMetadata(key: String, value: Any) {
Expand Down Expand Up @@ -150,6 +164,17 @@ public struct BitMovinPlayerView: View {
print("Setting category to AVAudioSessionCategoryPlayback failed.")
}
}


private func setup(title: String) {

// 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()
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import SwiftUI
public class BitmovinPlayerPlugin: VideoPlayerPlugin {

private let playerConfig: PlayerConfig
private var player: BitMovinPlayerView?
private weak var player: Player?

// Required properties
public let name: String
public let version: String
Expand All @@ -29,36 +29,49 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin {
self.version = "1.0.1"
}

func getPlayer() -> Player? {
return player?.player
}

// MARK: VideoPlayerPlugin protocol implementation
public func setup(config: VideoPlayerConfig) {
playerConfig.playbackConfig.isAutoplayEnabled = config.playbackConfig.autoplayEnabled
playerConfig.playbackConfig.isBackgroundPlaybackEnabled = config.playbackConfig.backgroundPlaybackEnabled

let uiConfig = BitmovinUserInterfaceConfig()
uiConfig.hideFirstFrame = true
playerConfig.styleConfig.userInterfaceConfig = uiConfig
}

public func playerView(hlsURLString: String, title: String = "") -> AnyView {
let videoPlayerView = BitMovinPlayerView(hlsURLString: hlsURLString, playerConfig: playerConfig, title: title)

self.player = videoPlayerView

return AnyView(videoPlayerView)

// Create player based on player and analytics configurations
let player = PlayerFactory.createPlayer(
playerConfig: playerConfig
)

self.player = player

return AnyView(
BitMovinPlayerView(
hlsURLString: hlsURLString,
player: player,
title: title
)
)
}

public func play() {
getPlayer()?.play()
player?.play()
}

public func pause() {
getPlayer()?.pause()
player?.pause()
}


public func unload() {
player?.unload()
}

public func removePlayer() {
player?.player.unload()
player?.player.destroy()
player = nil
player?.unload()
player?.destroy()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ public class VideoPlayerPluginManager: ObservableObject {
}

public func removePlugin() {
DispatchQueue.main.async {
self.selectedPlugin = nil
}
}
DispatchQueue.main.async {
self.selectedPlugin = nil
}
}
}

#endif
Expand Down
6 changes: 4 additions & 2 deletions Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ internal struct PlaybackUIView: View {
switch result {
case .success(let hlsURL):
print("HLS URL: \(hlsURL)")
self.videoURL = hlsURL
self.hasFetchedVideoDetails = true
DispatchQueue.main.async {
self.videoURL = hlsURL
self.hasFetchedVideoDetails = true
}
case .failure(let error):
// Trow error to the app
onError?(error)
Expand Down

0 comments on commit f7f7727

Please sign in to comment.