.onPlayPauseCommand is not working tvOS SwiftUI
06:02 24 Dec 2025

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.

  • SimplePlayer AKA 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 }
        }
    }
}
swiftui tvos