Удаленное управление транспортом не отображается для приложения SwiftUI с радиопотоком с AVPlayer - PullRequest
0 голосов
/ 12 июля 2020

Я разрабатываю простое приложение в SwiftUI для одного inte rnet радио. Он использует AVPlayer для воспроизведения потока, доступного по заданному URL-адресу. И это прекрасно работает. Я также настроил AVSession в AppDelegate, поэтому приложение воспроизводится в фоновом режиме, прекращает воспроизведение во время входящего вызова и возобновляет воспроизведение после вызова. Все работает нормально. Однако мне не удалось ни вывести пульт дистанционного управления на экран блокировки, ни показать приложение на плитке проигрывателя в Центре управления.

Приложение написано с использованием SwiftUI, я также перехожу от традиционных блоков завершения и целей к Объедините. Я создал отдельный класс Player, который является ObservableObject (и наблюдает за ContentView), где я настроил AVPlayer, AVPlayerItem (с заданным URL-адресом для потока). И все работает нормально. Приложение обновляет состояние при изменении состояния игрока. Я не использую AVPlayerViewController, поскольку он мне не нужен. При инициализации этого объекта Player я также настраиваю Remote Transport Controls, используя этот метод (я перешел от задания целей к издателям).

func setupRemoteTransportControls() {
        
        let commandCenter = MPRemoteCommandCenter.shared()
        
        commandCenter.publisher(for: \.playCommand)
            .sink(receiveValue: {_ in self.play() })
            .store(in: &cancellables)
        
        commandCenter.publisher(for: \.stopCommand)
            .sink(receiveValue: {_ in self.stop() })
            .store(in: &cancellables)
    }

Либо я использую исходную версию этого метода, предоставленную Apple, либо в моей собственной версии (как показано выше) пульт дистанционного управления не отображается, а проигрыватель плиток Центра управления не обновляется.

Конечно, я использую метод, предоставленный Apple для обновления NowPlaying

func setupNowPlaying() {
        var nowPlayingInfo = [String : Any]()
        nowPlayingInfo[MPMediaItemPropertyTitle] = "Radio"

        if let image = UIImage(systemName: "radio") {
            nowPlayingInfo[MPMediaItemPropertyArtwork] =
                MPMediaItemArtwork(boundsSize: image.size) { size in
                    return image
            }
        }
        
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentItem?.currentTime().seconds ?? ""
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = player?.currentItem?.asset.duration.seconds ?? ""
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1 : 0

        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }

Не знаю, в чем проблема. Это способ настройки удаленного управления транспортом? Процесс выглядит следующим образом:

Объект Observable Player с AVPlayer и настройкой для Remote Transport Controls и NowPlaying -> наблюдаемый -> Content View.

Вот полный список для класса Player:

import Foundation
import AVKit
import Combine
import MediaPlayer

class Player: ObservableObject {
    private let streamURL = URL(string: "https://stream.rcs.revma.com/ypqt40u0x1zuv")!
    
    @Published var status: Player.Status = .stopped
    @Published var isPlaying = false
    @Published var showError = false
    @Published var isMuted = false
    
    var player: AVPlayer?
        
    var cancellables = Set<AnyCancellable>()
    
    init() {
        setupRemoteTransportControls()
    }
    
    func setupPlayer() {
        let item = AVPlayerItem(url: streamURL)
        player = AVPlayer(playerItem: item)
        player?.allowsExternalPlayback = true
    }
    
    func play() {
        handleInterruption()
        handleRouteChange()
        setupPlayer()
        player?.play()
        player?.currentItem?.publisher(for: \.status)
            .sink(receiveValue: { status in
                self.handle(status: status)
            })
            .store(in: &cancellables)
    }
    
    func stop() {
        player?.pause()
        player = nil
        status = .stopped
    }
    
    func mute() {
        player?.isMuted.toggle()
        isMuted.toggle()
    }
    
    func handle(status: AVPlayerItem.Status) {
        switch status {
            case .unknown:
                self.status = .waiting
                self.isPlaying = false
            case .readyToPlay:
                self.status = .ready
                self.isPlaying = true
                self.setupNowPlaying()
            case .failed:
                self.status = .failed
                self.isPlaying = false
                self.showError = true
                self.setupNowPlaying()
            default:
                self.status = .stopped
                self.isPlaying = false
                self.setupNowPlaying()
        }
    }
    
    func handleInterruption() {
        NotificationCenter.default.publisher(for: AVAudioSession.interruptionNotification)
            .map(\.userInfo)
            .compactMap {
                $0?[AVAudioSessionInterruptionTypeKey] as? UInt
            }
            .map { AVAudioSession.InterruptionType(rawValue: $0)}
            .sink { (interruptionType) in
                self.handle(interruptionType: interruptionType)
            }
            .store(in: &cancellables)
    }
    
    func handle(interruptionType: AVAudioSession.InterruptionType?) {
        switch interruptionType {
        case .began:
            self.stop()
        case .ended:
            self.play()
        default:
            break
        }
    }
    
    typealias UInfo = [AnyHashable: Any]
    
    func handleRouteChange() {
        NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
            .map(\.userInfo)
            .compactMap({ (userInfo) -> (UInfo?, UInt?) in
                (userInfo, userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt)
            })
            .compactMap({ (result) -> (UInfo?, AVAudioSession.RouteChangeReason?) in
                (result.0, AVAudioSession.RouteChangeReason(rawValue: result.1 ?? 0))
            })
            .sink(receiveValue: { (result) in
                self.handle(reason: result.1, userInfo: result.0)
            })
            .store(in: &cancellables)
    }
    
    func handle(reason: AVAudioSession.RouteChangeReason?, userInfo: UInfo?) {
        switch reason {
        case .newDeviceAvailable:
            let session = AVAudioSession.sharedInstance()
            for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                DispatchQueue.main.async {
                    self.play()
                }
            }
        case .oldDeviceUnavailable:
            if let previousRoute = userInfo?[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
                for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
                    DispatchQueue.main.sync {
                        self.stop()
                    }
                    break
                }
            }
        default:
            break
        }
    }
}

extension Player {
    enum Status {
        case waiting, ready, failed, stopped
    }
}

extension Player {
    func setupRemoteTransportControls() {
        
        let commandCenter = MPRemoteCommandCenter.shared()
        
        commandCenter.publisher(for: \.playCommand)
            .sink(receiveValue: {_ in self.play() })
            .store(in: &cancellables)
        
        commandCenter.publisher(for: \.stopCommand)
            .sink(receiveValue: {_ in self.stop() })
            .store(in: &cancellables)
    }
    
    func setupNowPlaying() {
        var nowPlayingInfo = [String : Any]()
        nowPlayingInfo[MPMediaItemPropertyTitle] = "Radio"

        if let image = UIImage(systemName: "radio") {
            nowPlayingInfo[MPMediaItemPropertyArtwork] =
                MPMediaItemArtwork(boundsSize: image.size) { size in
                    return image
            }
        }
        
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentItem?.currentTime().seconds ?? ""
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = player?.currentItem?.asset.duration.seconds ?? ""
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1 : 0

        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
}

1 Ответ

0 голосов
/ 12 июля 2020

Оказалось, что мне нужно добавить в AppDelegate в application (: didFinishLaunchWithOptions) одну строку кода:

UIApplication.shared.beginReceivingRemoteControlEvents()

Это решило проблему. Теперь пульт ДУ отображается на экране блокировки, а также работает в Центре управления.

Одно дополнительное исправление. Изменение целей для издателя в setupRemoteTransportControls () в моем объекте Player не сработало. Поэтому я вернулся к установке таких целей.

func setupRemoteTransportControls() {
        
        let commandCenter = MPRemoteCommandCenter.shared()
        
        // Add handler for Play Command
        commandCenter.playCommand.addTarget { event in
            self.play()
            return .success
        }
        
        // Add handler for Pause Command
        commandCenter.pauseCommand.addTarget { event in
            self.stop()
            return .success
        }
    }
...