Swift
AVPlayer
iOS Development
Video Playback
Programming Tips

How do I get current playing time and total play time in AVPlayer?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

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

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

Formatting as mm:ss

swift
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:

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

swift
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:

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

swift
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

swift
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:

swift
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

swift
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

Course illustration
Course illustration

All Rights Reserved.