Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[ZEUS-4591] Fix video playing in background when switching #23

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 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 Expand Up @@ -197,7 +209,7 @@ public class PlayBackSDKManager {
/// - authorizationToken: Authorization token for accessing the video entry.
/// - completion: A closure to be called after loading the HLS stream.
/// It receives a result containing the HLS stream URL or an error.
internal func loadHLSStream(forEntryId entryId: String, andAuthorizationToken: String?, completion: @escaping (Result<URL, PlayBackAPIError>) -> Void) {
public func loadHLSStream(forEntryId entryId: String, andAuthorizationToken: String?, completion: @escaping (Result<URL, PlayBackAPIError>) -> Void) {
artem-y-pamediagroup marked this conversation as resolved.
Show resolved Hide resolved
guard let playBackAPIExist = playBackAPI else {
completion(.failure(PlayBackAPIError.initializationError))
return
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,16 @@ public struct BitMovinPlayerView: View {

return sConfig
}


public init(hlsURLString: String, player: Player, title: String) {
artem-y-pamediagroup marked this conversation as resolved.
Show resolved Hide resolved

self.hlsURLString = hlsURLString

self.player = player

setup(title: title)
}

public init(hlsURLString: String, playerConfig: PlayerConfig, title: String) {

self.hlsURLString = hlsURLString
Expand All @@ -35,21 +49,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 +68,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 +77,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 +146,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