AVPlayerItem step(byCount:) callback or notification

Hello there,

I need to move through video loaded in an AVPlayer one frame at a time back or forth. For that I tried to use AVPlayerItem's method step(byCount:) and it works just fine.

However I need to know when stepping happened and as far as I observed it is not immediate using the method. If I check the currentTime() just after calling the method it's the same and if I do it slightly later (depending of the video itself) it shows the correct "jumped" time.

To achieve my goal I tried subclassing AVPlayerItem and implement my own async method utilizing NotificationCenter and the timeJumpedNotification assuming it would deliver it as the time actually jumps but it's not the case.

Here is my "stripped" and simplified version of the custom Player Item:

import AVFoundation

final class PlayerItem: AVPlayerItem {
	private var jumpCompletion: ( (CMTime) -> () )?
	
	override init(asset: AVAsset, automaticallyLoadedAssetKeys: [String]?) {
		super .init(asset: asset, automaticallyLoadedAssetKeys: automaticallyLoadedAssetKeys)
		NotificationCenter.default.addObserver(self, selector: #selector(timeDidChange(_:)), name: AVPlayerItem.timeJumpedNotification, object: self)
	}
	
	deinit {
		NotificationCenter.default.removeObserver(self, name: AVPlayerItem.timeJumpedNotification, object: self)
		jumpCompletion = nil
	}
	
	@discardableResult func step(by count: Int) async -> CMTime {
		await withCheckedContinuation { continuation in
			step(by: count) { time in
				continuation.resume(returning: time)
			}
		}
	}
	
	func step(by count: Int, completion: @escaping ( (CMTime) -> () )) {
		guard jumpCompletion == nil else {
			completion(currentTime())
			return
		}
		jumpCompletion = completion
		step(byCount: count)
	}
	
	@objc private func timeDidChange(_ notification: Notification) {
		switch notification.name {
			case AVPlayerItem.timeJumpedNotification where notification.object as? AVPlayerItem [==](https://www.example.com/) self:
				jumpCompletion?(currentTime())
				jumpCompletion = nil
			default: return
		}
	}
}

In short the notification never gets called thus the above is not working. I guess the key there is that in the docs about the timeJumpedNotification: is said:

"A notification the system posts when a player item’s time changes discontinuously."

so the step(byCount:) is not considered as discontinuous operation and doesn't trigger it.

I'd be really helpful if somebody can help as I don't want to use seek(to:toleranceBefore:toleranceAfter:) mainly cause it's not accurate in terms of the exact next/previous frame as the video might have VFR and that causes repeating frames sometimes or even skipping one or another.

Thanks a lot

Precise stepping and seeking with AVFoundation is - in my experience - a very difficult to solve problem. I had to use AVSampleCursor to build a table of frame times and seek(to:toleranceBefore:toleranceAfter:) to get working results.

Quick update here.

First of all, thanks @Dirk-FU - I was thinking about that but held it for now as the app supports iOS 15+ where the AVSampleCursor is available from iOS 16.0+ as from the docs so I needed to find a common solution for all supported versions.

So... After playing around I found what was causing my issue as that same code worked for me just fine not long ago.

The notification never gets fired simply because the player item's canStepForward and/or canStepBackward are false thus the step(byCount:) doesn't move the time anywhere. So all works fine. Not how I needed it but correct.

Furthermore in my case the item cannot step back or forth simply because at some stage I switched to an AVQueuePlayer as I needed to be able to loop the video. Since the previous version used an AVPlayer instead and I was using a local variable for the PlayerItem (aka my custom AVPlayerItem) I was calling the method on that variable but it might not be the one currently used by the player thus the canStepForward and/or canStepBackward are false. Simply said an oversight on my side.

So the final change in my controller's code lead to all working fine again:


@discardableResult func step(_ direction: Direction) async -> Bool {
	guard let playerItem = player.currentItem as? PlayerItem else { return false }
	let currentTime = playerItem.currentTime()
	pause()
	switch direction {
		case .back:
			guard currentTime > .zero else { return false }
			await playerItem.step(by: -1)
		case .forward:
			guard currentTime < videoDuration else { return false }
			await playerItem.step(by: 1)
	}
	return true
}
AVPlayerItem step(byCount:) callback or notification
 
 
Q