I have an issue with updating playing info. Please have a look at the attached gif. In the end of the record playback sets to incorrect value.
I update the value by the key MPNowPlayingInfoPropertyElapsedPlaybackTime
from the timer block. I check if the value is valid, convert it to seconds and set the value. Value for MPMediaItemPropertyPlaybackDuration
is set from the initializer.
The closest solution I could find is to set the playback time again in playerDidFinishedPlaying
func. 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.
If you set playbackRate properly (default should be 1.0), nowPlayingInfoCenter updates the time itself.
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
. 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.
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.
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
}
})
}
}
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.