Introduction
AVPlayer provides the current playback position via currentTime() and the total duration via currentItem?.duration. Both return CMTime values, which you convert to seconds with CMTimeGetSeconds(). For a real-time progress display, use addPeriodicTimeObserver to get callbacks at regular intervals during playback. Handle edge cases like live streams (indefinite duration) and buffering states.
Getting Current Time and Duration
1import AVFoundation
2
3let player = AVPlayer(url: URL(string: "https://example.com/video.mp4")!)
4
5// Current playback position
6let currentTime = player.currentTime()
7let currentSeconds = CMTimeGetSeconds(currentTime)
8print("Current: \(currentSeconds)s")
9
10// Total duration
11if let duration = player.currentItem?.duration {
12 let totalSeconds = CMTimeGetSeconds(duration)
13 print("Total: \(totalSeconds)s")
14}
1func formatTime(_ seconds: Double) -> String {
2 guard seconds.isFinite else { return "--:--" }
3 let mins = Int(seconds) / 60
4 let secs = Int(seconds) % 60
5 return String(format: "%02d:%02d", mins, secs)
6}
7
8// Usage
9let current = CMTimeGetSeconds(player.currentTime())
10let total = CMTimeGetSeconds(player.currentItem?.duration ?? .zero)
11
12print("\(formatTime(current)) / \(formatTime(total))")
13// "01:23 / 05:47"
For videos over an hour, include hours:
1func formatTime(_ seconds: Double) -> String {
2 guard seconds.isFinite else { return "--:--" }
3 let hours = Int(seconds) / 3600
4 let mins = (Int(seconds) % 3600) / 60
5 let secs = Int(seconds) % 60
6
7 if hours > 0 {
8 return String(format: "%d:%02d:%02d", hours, mins, secs)
9 }
10 return String(format: "%02d:%02d", mins, secs)
11}
Periodic Time Observer (Real-Time Updates)
1class PlayerViewController: UIViewController {
2 var player: AVPlayer!
3 var timeObserver: Any?
4
5 override func viewDidLoad() {
6 super.viewDidLoad()
7
8 player = AVPlayer(url: URL(string: "https://example.com/video.mp4")!)
9
10 // Update every 0.5 seconds
11 let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
12 timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) {
13 [weak self] time in
14 guard let self = self else { return }
15
16 let current = CMTimeGetSeconds(time)
17 let total = CMTimeGetSeconds(self.player.currentItem?.duration ?? .zero)
18
19 self.currentTimeLabel.text = self.formatTime(current)
20 self.totalTimeLabel.text = self.formatTime(total)
21 self.progressSlider.value = Float(current / total)
22 }
23
24 player.play()
25 }
26
27 deinit {
28 // Always remove the observer
29 if let observer = timeObserver {
30 player.removeTimeObserver(observer)
31 }
32 }
33}
Waiting for Duration to Load
The duration is not immediately available — it loads asynchronously:
1// Method 1: KVO on duration
2player.currentItem?.observe(\.duration, options: [.new]) { item, _ in
3 let total = CMTimeGetSeconds(item.duration)
4 if total.isFinite {
5 print("Duration loaded: \(total)s")
6 }
7}
8
9// Method 2: Async/await (iOS 15+)
10Task {
11 let duration = try await player.currentItem?.asset.load(.duration)
12 if let duration = duration {
13 print("Duration: \(CMTimeGetSeconds(duration))s")
14 }
15}
16
17// Method 3: Check status
18if player.currentItem?.status == .readyToPlay {
19 let total = CMTimeGetSeconds(player.currentItem!.duration)
20 print("Ready. Duration: \(total)s")
21}
Progress Slider (Seek)
1@IBOutlet weak var slider: UISlider!
2
3// Update slider from player
4timeObserver = player.addPeriodicTimeObserver(
5 forInterval: CMTime(seconds: 0.1, preferredTimescale: 600),
6 queue: .main
7) { [weak self] time in
8 guard let self = self,
9 let duration = self.player.currentItem?.duration else { return }
10
11 let currentSeconds = CMTimeGetSeconds(time)
12 let totalSeconds = CMTimeGetSeconds(duration)
13
14 guard totalSeconds.isFinite else { return }
15 self.slider.value = Float(currentSeconds / totalSeconds)
16}
17
18// Seek when user drags slider
19@IBAction func sliderChanged(_ sender: UISlider) {
20 guard let duration = player.currentItem?.duration else { return }
21 let totalSeconds = CMTimeGetSeconds(duration)
22 let targetTime = CMTime(seconds: Double(sender.value) * totalSeconds,
23 preferredTimescale: 600)
24 player.seek(to: targetTime)
25}
Remaining Time
1func remainingTime() -> Double {
2 guard let duration = player.currentItem?.duration else { return 0 }
3 let total = CMTimeGetSeconds(duration)
4 let current = CMTimeGetSeconds(player.currentTime())
5 guard total.isFinite else { return 0 }
6 return total - current
7}
8
9// Display as "-03:24"
10let remaining = remainingTime()
11print("-\(formatTime(remaining))")
Handling Live Streams
Live streams have an indefinite duration:
1if let duration = player.currentItem?.duration {
2 if duration == .indefinite {
3 // Live stream — no total duration
4 totalTimeLabel.text = "LIVE"
5 progressSlider.isHidden = true
6 } else {
7 let total = CMTimeGetSeconds(duration)
8 totalTimeLabel.text = formatTime(total)
9 }
10}
Buffered Time
1func bufferedDuration() -> Double {
2 guard let timeRange = player.currentItem?.loadedTimeRanges.first?.timeRangeValue else {
3 return 0
4 }
5 let start = CMTimeGetSeconds(timeRange.start)
6 let duration = CMTimeGetSeconds(timeRange.duration)
7 return start + duration
8}
9
10// Show buffer progress
11let buffered = bufferedDuration()
12let total = CMTimeGetSeconds(player.currentItem?.duration ?? .zero)
13let bufferProgress = buffered / total
Common Pitfalls
Duration is NaN or indefinite before loading: The asset's duration is not available until the player item's status is .readyToPlay. Always check isFinite before using the value.
Not removing the time observer: Failing to call removeTimeObserver causes a retain cycle or crash. Always remove it in deinit or viewWillDisappear.
Using [self] instead of [weak self]: The periodic observer closure retains self strongly by default. Always use [weak self] to avoid retain cycles with the player.
Seeking during observation: Calling player.seek(to:) inside the periodic observer callback can cause stuttering. Pause observation during seek operations or debounce the calls.
Incorrect preferredTimescale: CMTime(seconds: 1.0, preferredTimescale: 1) has only 1-second precision. Use preferredTimescale: 600 for video (matches common frame rates) or CMTimeScale(NSEC_PER_SEC) for maximum precision.
Summary
Use player.currentTime() for current position and player.currentItem?.duration for total length
Convert CMTime to seconds with CMTimeGetSeconds()
Use addPeriodicTimeObserver for real-time UI updates (every 0.1-0.5 seconds)
Always check that duration is finite before using it (live streams return .indefinite)
Remove time observers in deinit to prevent retain cycles
Use [weak self] in observer closures to avoid memory leaks