MPNowPlayingInfoCenter nowPlayingInfo 在曲目结束时不更新

MPNowPlayingInfoCenter nowPlayingInfo not updating at end of track

我有一种方法可以更改我的应用 AVPlayer 播放的音轨,并为新曲目设置 MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo

func setTrackNumber(trackNum: Int) {
    self.trackNum = trackNum
    player.replaceCurrentItemWithPlayerItem(tracks[trackNum])

    var nowPlayingInfo: [String: AnyObject] = [ : ]        
    nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = tracks[trackNum].albumTitle
    nowPlayingInfo[MPMediaItemPropertyTitle] = "Track \(trackNum)"
    ...
    MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo = nowPlayingInfo 

    print("Now playing local: \(nowPlayingInfo)")
    print("Now playing lock screen: \(MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo)")   
}

当用户明确选择专辑或曲目以及曲目结束而下一首自动开始时,我会调用此方法。当用户设置专辑或曲目时,锁定屏幕会正确显示曲目元数据,但不会在曲目结束并自动设置下一曲目时显示。

我添加了打印语句以确保我正确地填充了 nowPlayingInfo 字典。正如预期的那样,当为用户启动的专辑或曲目更改调用此方法时,两个打印语句打印相同的字典内容。但是,在自动跟踪更改后调用该方法的情况下,本地 nowPlayingInfo 变量显示新的 trackNumMPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo 显示先前的 trackNum:

Now playing local: ["title": Track 9, "albumTitle": Test Album, ...]
Now playing set: Optional(["title": Track 8, "albumTitle": Test Album, ...]

我发现当我在 MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfonowPlayingInfo 的行上设置断点时,曲目编号会在锁定屏幕上正确更新。在该行之后添加 sleep(1) 还可以确保正确更新锁定屏幕上的曲目。

我已经验证 nowPlayingInfo 总是从主队列设置。我已经明确地 运行 在主队列或不同的队列中尝试使用此代码,但行为没有变化。

是什么阻碍了我对 MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo 的更改?如何确保设置 MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo 始终更新锁定屏幕信息?

编辑

在第 N 遍思考 "concurrency" 代码后,我找到了罪魁祸首。我不知道为什么我没有早点对此产生怀疑:

func playerTimeJumped() {
    let currentTime = currentItem().currentTime()

    dispatch_async(dispatch_get_main_queue()) {
        MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(currentTime)
    }
}

NSNotificationCenter.defaultCenter().addObserver(
       self,
       selector: "playerTimeJumped",
       name: AVPlayerItemTimeJumpedNotification,
       object: nil)

当用户滑动或跳过时,此代码更新锁定屏幕的经过时间 forward/back。如果我将其注释掉,setTrackNumbernowPlayingInfo 更新在任何情况下都会按预期工作。

修改后的问题:当这两段代码都运行在主队列中时它们是如何交互的?有什么办法可以在 AVPlayerItemTimeJumpedNotification 上进行 nowPlayingInfo 更新,因为在 setTrackNumber 上有呼叫时会有跳转?

对于背景信息更新,我的同事建议有必要进行一些实施。也许您可以在视图控制器中检查并验证其中一些要求:

    //1: Set true for canBecomeFirstResponder func
    override func canBecomeFirstResponder() -> Bool {
        return true
    }

    //2: Set view controller becomeFirstResponder & allow to receive remote control events
    override func viewDidLoad() {
        super.viewDidLoad()
        self.becomeFirstResponder()
        UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
        ....
    }

    //3: Implement actions after did receive events from remote control
    override func remoteControlReceivedWithEvent(event: UIEvent?) {
        guard let event = event else {
            return
        }
        switch event.subtype {
        case .RemoteControlPlay:
            ....
            break
        case .RemoteControlPause:
            ....
            break
        case .RemoteControlStop:
            ....
            break
        default:
            print("default action")
        }
    }

我无法对上述答案发表评论,但使用 MPRemoteCommandCenter 时,无需调用 -[UIApplication beginReceivingRemoteControlEvents]-[UIResponder becomeFirstResponder] 来处理远程事件。以上答案指的是不再推荐的旧实现。

建议在nowPlayingInfo字典中设置尽可能多的键。 MPMediaItemPropertyPlaybackDurationMPNowPlayingInfoPropertyPlaybackRateMPNowPlayingInfoPropertyElapsedPlaybackTime 会影响 MPNowPlayingInfoCenter 是否更新。

你能试试这个代码吗?这适用于我的示例...

override func viewDidLoad() {
super.viewDidLoad()

if NSClassFromString("MPNowPlayingInfoCenter") != nil {
    let albumArt = MPMediaItemArtwork(image: image) // any image
    var songInfo: NSMutableDictionary = [
        MPMediaItemPropertyTitle: "Whatever",
        MPMediaItemPropertyArtist: "Whatever",
        MPMediaItemPropertyArtwork: albumArt
    ]
    MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo = songInfo as [NSObject : AnyObject]
}
try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: [])
   try! AVAudioSession.sharedInstance().setActive(true)
}

说明: 您必须检查 MPNowPlayingInfo 是否处于活动状态,因为它有时会进入后台。如果它在后台,那么你必须激活它来执行这行代码:

try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: [])
       try! AVAudioSession.sharedInstance().setActive(true)

如果有效请写信给我...

编辑

如果这不起作用,您也可以尝试此代码,但上面的代码是更现代的解决方案

if (AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, error: nil)) {
    println("Receiving remote control events")
    UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
} else {
    println("Audio Session error.")
}

这里也是尝试激活,同上。这是旧版本,可能无法使用...

首先,一定要在 .plist 文件中为您的应用启用后台模式。这将允许您的应用在锁定时使用后台任务和 运行 您的更新代码。

其次,如果您想在适当的时间更新它,我会让 AVAudioPlayer 委托函数调用更新函数 audioPlayerDidFinishPlaying:successfully:。您也可以尝试注册一个已完成的通知作为替代。

问题是nowPlayingInfo在轨道自动改变时同时在两个地方更新:在setTrackNumber方法中,由AVPlayerItemDidPlayToEndTimeNotification触发,在playerTimeJumped 方法由 AVPlayerItemTimeJumpedNotification.

触发

这会导致竞争条件。 Apple 工作人员 here.

提供了更多详细信息

这个问题可以通过保留本地 nowPlayingInfo 字典来解决,该字典会根据需要进行更新,并始终根据该字典设置 MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo 而不是单独设置值。

这是我的案例:

AVAudioSession 很重要。


不是这个:

let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(AVAudioSession.Category.playback, options: AVAudioSession.CategoryOptions.mixWithOthers)

            audioSession.requestRecordPermission({ (isGranted: Bool) in  })

            try AVAudioSession.sharedInstance().setActive(true, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)

        } catch  {

        }

没用


但是

       do {
            //keep alive audio at background
            try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
        } catch _ { }

        do {
            try AVAudioSession.sharedInstance().setActive(true)
        } catch _ { }

有效