.onPlayPauseCommand is not working tvOS SwiftUI
I'm working on tvOS app with SwiftUI, when Player is first time loaded .onPlayPauseCommand is working perfectly. But when IMAManager advertisement loaded and ends, .onPlayPauseCommand stops working. Except .onPlayPauseCommand rest commands are working perfectly.
SimplePlayerAKA MainPlayer Frame
final class SimplePlayer: ObservableObject {
let player: AVPlayer
var imaManager: IMAManager?
@Published private(set) var isPlaying: Bool = false
@Published private(set) var duration: Double = 0
@Published private(set) var currentTime: Double = 0
@Published var isSeeking: Bool = false
var onFinished: (() -> Void)?
var onPlaying: (() -> Void)?
var onChangeAdStatus: ((_ adCompleted: Bool) -> Void)?
private var timeObserver: Any?
private var cancellables = Set()
@Published private(set) var isReadyToPlay: Bool = false
private var statusObserver: NSKeyValueObservation?
init(url: URL) {
let deviceId = UIDevice().identifierForVendor?.uuidString ?? ""
let requestTime = String(Int(Date().timeIntervalSince1970))
let stringToHash = deviceId + "APPLETV" + requestTime
let hash = stringToHash.MD5
let headers: [String : String] = [
"referer": "https://myPersonalHeader.come/"
]
// MARK: AVAsset
let asset = AVURLAsset(
url: url,
options: ["AVURLAssetHTTPHeaderFieldsKey": headers]
)
// Monitor asset load failure
asset.loadValuesAsynchronously(forKeys: ["playable"]) {
var error: NSError?
let status = asset.statusOfValue(forKey: "playable", error: &error)
if status == .failed {
print("AVAsset load error: \(error?.localizedDescription ?? "Unknown")")
}
}
let item = AVPlayerItem(asset: asset)
self.player = AVPlayer(playerItem: item)
// Observe playback failure
NotificationCenter.default.addObserver(
forName: .AVPlayerItemFailedToPlayToEndTime,
object: item,
queue: .main
) { notif in
let err = notif.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? NSError
print("Playback failed: \(err?.localizedDescription ?? "Unknown error")")
}
// Observe item.status
statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
DispatchQueue.main.async {
if item.status == .readyToPlay {
self?.isReadyToPlay = true
} else if item.status == .failed {
self?.isReadyToPlay = false
print("PlayerItem status error: \(item.error?.localizedDescription ?? "Unknown")")
}
}
}
observeTime()
observeItemEnd()
}
deinit {
if let token = timeObserver {
player.removeTimeObserver(token)
}
cancellables.removeAll()
statusObserver?.invalidate()
statusObserver = nil
}
// periodic time to update UI
private func observeTime() {
let interval = CMTime(seconds: 0.25, preferredTimescale: 600)
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
guard let self = self, !self.isSeeking else { return }
self.currentTime = time.seconds
if let dur = self.player.currentItem?.duration.seconds, dur.isFinite {
self.duration = dur
}
}
}
private func observeItemEnd() {
NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: self.player.currentItem)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self = self else { return }
self.player.seek(to: .zero)
self.onFinished?()
}
.store(in: &cancellables)
}
func play() {
player.play()
isPlaying = true
onPlaying?()
}
func pause() {
player.pause()
isPlaying = false
}
func togglePlayPause() {
if imaManager?.adState != IMAManager.AdPlayingStatus.none {
imaManager?.adState == .playing ? imaManager?.pause() : imaManager?.resume()
} else {
isPlaying ? pause() : play()
}
}
func seek(to seconds: Double) {
let time = CMTime(seconds: seconds, preferredTimescale: 600)
isSeeking = true
player.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] _ in
guard let self = self else { return }
self.isSeeking = false
self.currentTime = seconds
}
}
func setupIMA(adContainer: UIView, hostViewController: UIViewController) {
imaManager = IMAManager(player: player, adContainer: adContainer, hostViewController: hostViewController)
imaManager?.didRequestContentPause = { [weak self] in
self?.pause()
}
imaManager?.didRequestContentResume = { [weak self] in
self?.play()
self?.onChangeAdStatus?(true)
}
imaManager?.didReceiveEvent = { [weak self] event in
if event == .STARTED || event == .PAUSE || event == .RESUME {
self?.isPlaying = self?.imaManager?.areAdsPlaying() ?? false
}
if event == .STARTED || event == .COMPLETE || event == .SKIPPED {
let adCompleted = event == .COMPLETE || event == .SKIPPED
self?.onChangeAdStatus?(adCompleted)
}
}
}
func requestAds(tag: String) {
imaManager?.requestAds(from: tag)
}
func destroyIMA() {
imaManager?.destroy()
imaManager = nil
}
}
AVPlayerLayer
struct PlayerViewControllerView: UIViewControllerRepresentable {
let player: AVPlayer
var showsPlaybackControls: Bool = true
var onAdEnvironmentReady: ((UIView, UIViewController) -> Void)?
func makeUIViewController(context: Context) -> AVPlayerViewController {
let vc = AVPlayerViewController()
vc.player = player
vc.showsPlaybackControls = showsPlaybackControls
#if os(tvOS)
vc.videoGravity = .resizeAspect
vc.requiresLinearPlayback = false
#endif
DispatchQueue.main.async {
self.onAdEnvironmentReady?(vc.view, vc)
}
return vc
}
func updateUIViewController(_ vc: AVPlayerViewController, context: Context) {
vc.player = player
vc.showsPlaybackControls = showsPlaybackControls
}
}
- VerticalPLayer
struct VerticalPlayerView: View {
@ObservedObject var player: SimplePlayer
// Controls
@Binding var showControls: Bool
@State private var hideTimer: Timer?
@FocusState private var playPauseFocused: Bool
@Environment(\.scenePhase) private var scenePhase
@Namespace private var playerFocusScope
private let sliderLift: CGFloat = 120
var body: some View {
GeometryReader { geo in
ZStack {
#if os(iOS)
PlayerLayerView(player: player.player) { adView, viewController in
player.setupIMA(adContainer: adView, hostViewController: viewController)
}
.ignoresSafeArea()
.onTapGesture { toggleControls() }
.onDisappear {
player.destroyIMA()
}
#endif
#if os(tvOS)
PlayerViewControllerView(
player: player.player,
showsPlaybackControls: true
) { adView, viewController in
player.setupIMA(
adContainer: adView,
hostViewController: viewController
)
}
.ignoresSafeArea()
.onDisappear {
player.destroyIMA()
}
#endif
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.6).opacity(!player.isReadyToPlay ? 1 : 0.000)
// Center Play/Pause
if showControls {
VStack {
Spacer()
LinearGradient(
gradient: Gradient(colors: [
Color.black.opacity(0.0002),
Color.black.opacity(0.5),
Color.black.opacity(0.9)
]),
startPoint: .top,
endPoint: .bottom
)
.frame(height: SizeScaler.height(180))
.ignoresSafeArea(edges: .bottom)
}
.allowsHitTesting(false) // so touches pass through
if player.isReadyToPlay{
VStack {
Spacer()
Button {
player.togglePlayPause()
resetHideTimer()
} label: {
ZStack {
Circle()
.fill(Color.black.opacity(0.45))
.frame(width: 76, height: 76)
Image(systemName: player.isPlaying ? "pause.fill" : "play.fill")
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
.foregroundColor(.white)
}
}
.buttonStyle(.plain)
Spacer()
}
.transition(.opacity)
}
}
// SLIDER HERE (inside player)
#if os(iOS)
if showControls && player.imaManager?.adState == IMAManager.AdPlayingStatus.none {
VStack {
Spacer()
VideoProgressBar(
value: Binding(
get: { player.currentTime },
set: { newValue in player.seek(to: newValue) }
),
range: 0...max(player.duration, 1),
onEditingChanged: { editing in
if editing { hideTimer?.invalidate() }
else { resetHideTimer() }
}
).padding(.bottom, UIApplication.shared.keyWindowInsets.bottom + SizeScaler.width(15))
.padding(.horizontal, SizeScaler.width(16))
.transition(.opacity)
}.zIndex(999999)
}
#endif
}
#if os(tvOS)
.focusScope(playerFocusScope)
#endif
.onAppear { resetHideTimer() }
.onDisappear { hideTimer?.invalidate() }
.onChange(of: scenePhase) { new in
if new != .active {
player.pause()
}
}
}
#if os(tvOS)
.onPlayPauseCommand { // <----- Here, it is working first time, but when imaHandler ends first time this is not working
if !showControls {
showControls = true
playPauseFocused = true
}
player.togglePlayPause()
resetHideTimer()
}
.onMoveCommand { _ in
if !showControls {
showControls = true
playPauseFocused = true
}
resetHideTimer()
}
#endif
}
private func toggleControls() {
withAnimation { showControls.toggle() }
#if os(tvOS)
if showControls {
playPauseFocused = true
}
#endif
if showControls { resetHideTimer() }
else { hideTimer?.invalidate() }
}
private func resetHideTimer() {
hideTimer?.invalidate()
hideTimer = Timer.scheduledTimer(withTimeInterval: 4, repeats: false) { _ in
withAnimation { showControls = false }
}
}
}