MPNowPlayingInfoCenter现在播放信息在曲目结束时没有更新

15

我有一个方法可以更改我的应用程序的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变量显示新的trackNum,而MPNowPlayingInfoCenter.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次检查代码后,我想到了“并发”,找出了罪魁祸首。我不知道为什么之前没有怀疑过这个:

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)

这段代码在用户快进/倒退时更新锁屏的已过时间。如果将其注释掉,则在任何情况下,从setTrackNumber获取的nowPlayingInfo更新都能按预期工作。

修订后的问题:当它们都在主队列上运行时,这两个代码片段是如何交互的?是否有任何方法可以在AVPlayerItemTimeJumpedNotification上进行nowPlayingInfo更新,假设在调用setTrackNumber时会发生跳跃?

1
有其他人经历过类似的情况吗? - Hélène Martin
1
我在这里推测,但 MPNowPlayingInfoCenter 必须向管理锁屏的任何系统进程分派 nowPlayingInfo 更改。所以你的调试输出一点也不让我惊讶…… setNowPlayingInfo 方法实际上是异步完成的。 - Reuben Scratton
当音轨自动更改时,setTrackNumber是否每次都被调用?即使应用程序在后台运行? - Reuben Scratton
@ReubenScratton - 我同意设置 nowPlayingInfo 可能是异步完成的,因此打印输出并不是很有帮助。但我期望设置会在某个时刻完成!当手机显示锁屏界面时,我知道 setTrackNumber 总是被调用的,因为我的调试打印信息出现在控制台上。 - Hélène Martin
@HélèneMartin 不确定是否相关。只是想知道是否在“能力”中打开了“后台模式”? - Allen
显示剩余4条评论
6个回答

11
问题在于当音轨自动更改时,nowPlayingInfo 在两个位置同时更新:一个是在由 AVPlayerItemDidPlayToEndTimeNotification 触发的 setTrackNumber 方法中,另一个是在由 AVPlayerItemTimeJumpedNotification 触发的 playerTimeJumped 方法中。

这会引起竞态条件。有关更多详细信息,请参见苹果员工此处提供的说明。
此问题可以通过保留本地 nowPlayingInfo 字典来解决,在需要时进行更新,并始终从该字典设置 MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo,而不是单独设置值。

1

为了更新背景信息,我的同事建议需要进行一些实现。也许您可以在您的视图控制器中检查和验证其中一些要求:

    //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")
        }
    }

我处理#2。#3不应该直接相关,因为它涉及远程控制事件,但是是的,我确实正确处理远程控制事件。至于#1和self.becomeFirstResponder(),我不完全理解。我认为这是为了使视图能够接收远程控制事件?setTrackNumber方法在一个包装AVPlayer的类中,并且我在我的视图中使用单例。 - Hélène Martin
@HélèneMartin 是的,我知道你的意思。一开始我也是这么想的;但是,当我查看苹果公司的iOS事件处理指南(其中还包括响应者链的主题)时,我发现“提供当前播放信息”这个主题在“远程控制事件”一节下面。https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Remote-ControlEvents/Remote-ControlEvents.html#//apple_ref/doc/uid/TP40009541-CH7-SW1 因此,我认为背景信息更新可能是远程控制设计概念的一部分。 - Allen
@HélèneMartin 如果您正在iOS模拟器中进行测试,请根据文档始终在nowPlayingInfo字典中包含MPNowPlayingInfoPropertyPlaybackRate键。 - Allen

1

你能试试这段代码吗?它在我的示例中有效...

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.")
}

在这里,您也正在尝试使其活动,与上面的相同。 这是一个旧版本,可能无法正常工作...


我设置了会话的类别并使其处于活动状态。请注意,尽管应用程序在前台运行时,由上一个曲目结束自动触发的曲目更改从不正确地更新nowPlayingInfo。 - Hélène Martin

1

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

其次,如果您想要在正确的时间更新它,则可以让更新函数通过AVAudioPlayer委托函数audioPlayerDidFinishPlaying:successfully:来调用。另外,您也可以尝试注册一个完成通知作为替代方法。


后台模式已启用并正常工作。我使用AVPlayer,并且通过AVPlayerItemDidPlayToEndTimeNotification触发setTrackNumber调用。如您在我的修订中所见,问题在于两个不同的通知有时会同时触发对nowPlayingInfo的更新。 - Hélène Martin

0

我无法对上面的答案进行评论,但是在使用MPRemoteCommandCenter时,不需要调用-[UIApplication beginReceivingRemoteControlEvents]-[UIResponder becomeFirstResponder]来处理远程事件。上面的答案是指旧的实现方式,不再推荐使用。

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


没错 - 我使用了新的远程控制事件实现,它们正常工作。我设置了所有我拥有信息的 nowPlayingInfo 键,包括你提到的所有键。 - Hélène Martin

0
这是我的情况: < p > 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 _ { }

它运行正常


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接