Я разрабатываю простое приложение в 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
}
}