Проблема с обновлением времени воспроизведения в MPNowPlayingInfoCenter из блока наблюдателя периодического времени проигрывателя AVPlayer

У меня проблема с обновлением игровой информации. Пожалуйста, взгляните на прикрепленный gif. В конце записи воспроизведение устанавливается на неверное значение.

gif

Обновляю значение по ключу MPNowPlayingInfoPropertyElapsedPlaybackTime из блока таймера. Я проверяю, действительно ли значение, конвертирую его в секунды и устанавливаю значение. Значение для MPMediaItemPropertyPlaybackDuration устанавливается из инициализатора.

Самое близкое решение, которое я смог найти, — снова установить время воспроизведения в playerDidFinishedPlaying func. В этом случае ползунок прогресса доходит до конца, но я все еще вижу, что он на мгновение отскакивает назад.

Вот реализация плеера:

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
    }
}


person AlexEdunov    schedule 12.02.2020    source источник
comment
У меня точно такая же проблема. Похоже на баг iOS(   -  person algrid    schedule 20.09.2020


Ответы (3)


Вам не нужно устанавливать прошедшее время снова и снова.
Просто обновите elapsedTime, когда вы начинаете воспроизводить новый объект или после поиска.
Если вы правильно установили PlayPlayRate (по умолчанию должно быть 1,0), теперь PlayingInfoCenter обновит само время.

person Robin Schmidt    schedule 12.02.2020
comment
@AlexEdunov тебе это помогло? - person Robin Schmidt; 13.02.2020

У меня точно такая же проблема. Чтобы смягчить это, я обновляю информацию о текущем воспроизведении (скорость воспроизведения/длительность/позиция), когда свойство timeControlStatus игрока становится paused. Что-то вроде этого:

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

    })

Таким образом, у меня не будет неприятного прыжка назад, о котором вы говорите.

person algrid    schedule 20.09.2020

У меня такая же проблема...

Я бы обновлял время воспроизведения в фоновом режиме, когда для переменной currentTime проигрывателя было установлено другое время.

Поскольку это было мое приложение, обновляющее значение без ввода пользователя, оно просто никогда не работало. Я был почти уверен, что никогда не заработаю.

НАКОНЕЦ - то, что сработало для меня. Сначала пара правил.

  1. Всегда сохраняйте собственную копию всего информационного словаря и обновляйте ее.
  2. Установите весь информационный словарь одновременно (т.е. не устанавливайте отдельные ключи в MPNowPlayingInfoCenter.default().nowPlayingInfo напрямую. Это не работает так, как вам хотелось бы.) Обновите все это.
  3. НЕ обновляйте словарь несколько раз подряд или в быстрой последовательности. Поэтому, если у вас есть методы для обновления скорости и времени воспроизведения, которые часто вызываются вместе, убедитесь, что вы обновляете оба ключа в своем локальном словаре и устанавливаете nowPlayingInfo только один раз.

Например, вот некоторый код от меня для размышления.

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

}
person TheJeff    schedule 07.02.2021