简体   繁体   English

从 AVPlayer 播放器定期时间观察器块更新 MPNowPlayingInfoCenter 中的播放时间的问题

[英]Issue with updating playback time in MPNowPlayingInfoCenter from AVPlayer player periodic time observer block

I have an issue with updating playing info.我在更新播放信息时遇到问题。 Please have a look at the attached gif.请看一下附件中的gif。 In the end of the record playback sets to incorrect value.在记录播放结束时设置为不正确的值。

gif

I update the value by the key MPNowPlayingInfoPropertyElapsedPlaybackTime from the timer block.我通过来自计时器块的键MPNowPlayingInfoPropertyElapsedPlaybackTime更新该值。 I check if the value is valid, convert it to seconds and set the value.我检查该值是否有效,将其转换为秒并设置该值。 Value for MPMediaItemPropertyPlaybackDuration is set from the initializer. MPMediaItemPropertyPlaybackDuration值从初始值设定MPMediaItemPropertyPlaybackDuration中设置。

The closest solution I could find is to set the playback time again in playerDidFinishedPlaying func.我能找到的最接近的解决方案是在playerDidFinishedPlaying函数中再次设置播放时间。 In this case the progress slider goes to the end but I still can see that it jumps back for a moment.在这种情况下,进度滑块会走到最后,但我仍然可以看到它向后跳了一会儿。

Here is an implementation of player:这是播放器的一个实现:

import AVFoundation
import MediaPlayer

class Player {
    var onPlay: (() -> Void)?
    var onPause: (() -> Void)?
    var onStop: (() -> Void)?
    var onProgressUpdate: ((Float) -> Void)?
    var onTimeUpdate: ((TimeInterval) -> Void)?
    var onStartLoading: (() -> Void)?
    var onFinishLoading: (() -> Void)?

    private var player: AVPlayer?
    private var timeObserverToken: Any?
    private var durationSeconds: Float64 = 0

    private static let preferredTimescale = CMTimeScale(NSEC_PER_SEC)
    private static let seekTolerance = CMTimeMakeWithSeconds(1, preferredTimescale: preferredTimescale)

    private var nowPlayingInfo = [String : Any]() {
        didSet {
            MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
        }
    }

    init(url: URL, name: String) {
        self.nowPlayingInfo[MPMediaItemPropertyTitle] = name

        let asset = AVURLAsset(url: url)

        onStartLoading?()

        asset.loadValuesAsynchronously(forKeys: ["playable"]) { [weak self] in
            guard let self = self else { return }

            let durationSeconds = CMTimeGetSeconds(asset.duration)

            self.durationSeconds = durationSeconds
            self.nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Int(durationSeconds)

            let playerItem = AVPlayerItem(asset: asset)

            let player = AVPlayer(playerItem: playerItem)
            player.actionAtItemEnd = .pause

            self.player = player

            self.configureStopObserver()
            self.setupRemoteTransportControls()

            self.onTimeUpdate?(0)
            self.onFinishLoading?()
        }
    }

    func seek(progress: Float) {
        guard let player = player else { return }

        let targetTimeValue = durationSeconds * Float64(progress)
        let targetTime = CMTimeMakeWithSeconds(targetTimeValue, preferredTimescale: Self.preferredTimescale)

        let tolerance = CMTimeMakeWithSeconds(1, preferredTimescale: Self.preferredTimescale)

        player.seek(to: targetTime, toleranceBefore: tolerance, toleranceAfter: tolerance)
    }

    func playPause() {
        guard let player = player else { return }

        if player.isPlaying {
            player.pause()

            onPause?()
        } else {
            let currentSeconds = CMTimeGetSeconds(player.currentTime())

            if durationSeconds - currentSeconds < 1 {
                let targetTime = CMTimeMakeWithSeconds(0, preferredTimescale: Self.preferredTimescale)

                player.seek(to: targetTime)
            }

            player.play()

            onPlay?()
        }
    }

    private func configureStopObserver() {
        guard let player = player else { return }

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(playerDidFinishedPlaying),
                                               name: .AVPlayerItemDidPlayToEndTime,
                                               object: player.currentItem)

    }

    @objc private func playerDidFinishedPlaying() {
        guard let player = player else { return }

        let currentSeconds = CMTimeGetSeconds(player.currentTime())

        self.onTimeUpdate?(TimeInterval(currentSeconds))

        // self.nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Int(currentSeconds)
        // self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate

        onStop?()
    }

    func handleAppearing() {
        subscribeToTimeObserver()
        configureAndActivateAudioSession()
    }

    func handleDisappearing() {
        unsubscribeFromTimeObserver()
        deactivateAudioSession()
    }

    private func configureAndActivateAudioSession() {
        let audioSession = AVAudioSession.sharedInstance()

        try? audioSession.setCategory(.playback, mode: .default, options: [])
        try? audioSession.setActive(true, options: [])
    }

    private func deactivateAudioSession() {
        guard let player = player else { return }

        player.pause()

        try? AVAudioSession.sharedInstance().setActive(false, options: [])
    }

    private func subscribeToTimeObserver() {
        guard let player = player else { return }

        let preferredTimescale = CMTimeScale(NSEC_PER_SEC)

        let interval = CMTimeMakeWithSeconds(0.1, preferredTimescale: preferredTimescale)

        timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: nil, using: { [weak self] time in
            guard let self = self else { return }

            let timeIsValid = time.flags.rawValue & CMTimeFlags.valid.rawValue == 1
            let timeHasBeenRounded = time.flags.rawValue & CMTimeFlags.hasBeenRounded.rawValue == 1

            if !timeIsValid && !timeHasBeenRounded {
                return
            }

            let currentSeconds = CMTimeGetSeconds(time)

            self.onTimeUpdate?(TimeInterval(currentSeconds))

            self.nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Int(currentSeconds)
            self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate

            let progress = Float(currentSeconds / self.durationSeconds)

            self.onProgressUpdate?(progress)
        })
    }

    private func unsubscribeFromTimeObserver() {
        if let token = timeObserverToken, let player = player {
            player.removeTimeObserver(token)
        }
    }

    private func setupRemoteTransportControls() {
        let commandCenter = MPRemoteCommandCenter.shared()

        commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
            guard
                let self = self,
                let player = self.player

                else { return .commandFailed }

            if let event = event as? MPChangePlaybackPositionCommandEvent {
                let targetTime = CMTimeMakeWithSeconds(event.positionTime, preferredTimescale: Self.preferredTimescale)

                player.seek(to: targetTime, toleranceBefore: Self.seekTolerance, toleranceAfter: Self.seekTolerance)

                return .success
            }

            return .commandFailed
        }

        commandCenter.playCommand.addTarget { [weak self] event in
            guard
                let self = self,
                let player = self.player

                else { return .commandFailed }

            if !player.isPlaying {
                player.play()
                self.onPlay?()

                return .success
            }

            return .commandFailed
        }

        commandCenter.pauseCommand.addTarget { [weak self] event in
            guard
                let self = self,
                let player = self.player

                else { return .commandFailed }

            if player.isPlaying {
                player.pause()
                self.onPause?()

                return .success
            }

            return .commandFailed
        }
    }
}

private extension AVPlayer {
    var isPlaying: Bool {
        return rate != 0 && error == nil
    }
}

You don't need to set the elapsed time over and over again.您不需要一遍又一遍地设置经过的时间。
Just update elapsedTime when you start playing a new asset or after seeking.只需在开始播放新资产或搜索后更新 elapsedTime。
If you set playbackRate properly (default should be 1.0), nowPlayingInfoCenter updates the time itself.如果您正确设置了播放速率(默认值应为 1.0),则 nowPlayingInfoCenter 会自行更新时间。

I have exactly the same problem.我有完全一样的问题。 To mitigate it I update the now playing info (playback rate/duration/position) when the player's timeControlStatus property becomes paused .为了缓解它,我会在玩家的timeControlStatus属性变为paused时更新正在播放的信息(播放速率/持续时间/位置)。 Something like this:像这样的东西:

    observations.append(player.observe(\.timeControlStatus, options: [.old, .new]) { [weak self] (player, change) in

        switch player.timeControlStatus {
        case .paused:
            self?.updateNowPlayingInfo()
        case .playing:
            self?.updateNowPlayingInfo()
        case .waitingToPlayAtSpecifiedRate:
            break
        default:
            DDLogWarn("Player: unknown timeControlStatus value")
        }

    })

This way I don't have the unpleasant jump back you're talking about.这样我就不会像你所说的那样令人不快地跳回去。

I had the same problem...我有同样的问题...

For me, I'd update the playback time in the background when the player's currentTime variable was set to a different time.对我来说,当播放器的 currentTime 变量设置为不同的时间时,我会在后台更新播放时间。

Since it was my app updating the value without input from the user, it just never worked.由于它是我的应用程序在没有用户输入的情况下更新值,因此它从未起作用。 I was pretty convinced I'd never get it working.我非常确信我永远不会让它工作。

FINALLY - something that worked for me.最后 - 对我有用的东西。 First a couple rules.首先是几个规则。

  1. Always keep your own copy of the whole info dictionary and update that.始终保留您自己的整个信息字典的副本并进行更新。
  2. Set the whole info dictionary at the same time (IE don't set individual keys on the MPNowPlayingInfoCenter.default().nowPlayingInfo directly. It doesn't work as well as you'd like.) Update the whole thing.同时设置整个信息字典(IE 不直接在 MPNowPlayingInfoCenter.default().nowPlayingInfo 上设置单个键。它不能像您希望的那样工作。)更新整个内容。
  3. DO NOT update the dictionary multiple times in succession or rapid-succession.不要连续或快速连续多次更新字典。 So if you have methods to update the rate and playback time that frequently get called together, be very sure that you're only updating both keys in your local dictionary and setting the nowPlayingInfo once.因此,如果您有更新经常一起调用的速率和播放时间的方法,请确保您只更新本地字典中的两个键并设置 nowPlayingInfo 一次。

Eg - here is some code from me to ya'll for pondering.例如 - 这是我发给你们的一些代码,供您思考。

class InfoCenter {

private static var info = [String : Any]()

class func initNowPlayingInfo(_ param: Any) {
    // setup your info dictionary here
    DispatchQueue.main.async(execute: {
        MPNowPlayingInfoCenter.default().nowPlayingInfo = info
    })
}

class func syncPlaybackRate(setInfo set: Bool) {
    DispatchQueue.main.async(execute: {
        if UIApplication.shared.applicationState != .inactive {
            if let player = player {
                info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentPlaybackRate
                if set {
                    MPNowPlayingInfoCenter.default().nowPlayingInfo = info
                }
            }
        }
    })
}

class func syncPlaybackTime(setInfo set: Bool) {
    if UIApplication.shared.applicationState != .inactive {
        if let player = player {
            DispatchQueue.main.async(execute: {
                if let song = song {
                    info[MPNowPlayingInfoPropertyPlaybackProgress] = player.currentPlaybackTime / song.getEndTime()
                }
                info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentPlaybackTime
                if set {
                    MPNowPlayingInfoCenter.default().nowPlayingInfo = info
                }
            })
        }
    }
}

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM