The behavior of AVPlayerItem.didPlayToEndTimeNotification is not as expected in iOS 26.

Hello,

Environment macOS 15.6.1 / Xcode 26 beta 7 / iOS 26 Beta 9

In a simple AVFoundation video-playback sample, I’m seeing different behavior between iOS 18 and iOS 26 regarding AVPlayerItem.didPlayToEndTimeNotification.

I’ve attached a minimal sample below. Please replace videoURL with a valid short video URL.

Repro steps

  1. Tap “Play” to start playback and let the video finish. The AVPlayerItem.didPlayToEndTimeNotification registered with NotificationCenter should fire, and you should see Play finished. in the console.

  2. Without relaunching, tap “Play” again. This is where the issue arises.

Observed behavior

  • On iOS 18 and earlier: The video does not play again (it does not restart from the beginning), but AVPlayerItem.didPlayToEndTimeNotification is posted and Play finished. appears in the console. The same happens every time you press “Play”.

  • On iOS 26: Pressing “Play” does not post AVPlayerItem.didPlayToEndTimeNotification. The code path that prints Play finished. is never called (the callback enclosing that line is not invoked again).

Building the same program with Xcode 16.4 and running it on an iOS 26 beta device shows the same phenomenon, which suggests there has been a behavioral change for AVPlayerItem.didPlayToEndTimeNotification on iOS 26. I couldn’t find any mention of this in the release notes or API Reference.

Because the semantics around AVPlayerItem.didPlayToEndTimeNotification appear to differ, we’re forced to adjust our logic. If there is a way to achieve the iOS 18–style behavior on iOS 26, I would appreciate guidance.

Alternatively, if this change is intentional, could you share the reasoning? Is iOS 26 the correct behavior from Apple’s perspective and iOS 18 (and earlier) behavior considered incorrect? Any official clarification would be extremely helpful.

import UIKit
import AVFoundation

final class ViewController: UIViewController {
    
    private let videoURL = URL(string: "https://......mp4")!
    private var player: AVPlayer?
    private var playerItem: AVPlayerItem?
    private var playerLayer: AVPlayerLayer?

    private var observeForComplete: NSObjectProtocol?

    // UI
    private let playerContainerView = UIView()
    private let playButton = UIButton(type: .system)
    private let stopButton = UIButton(type: .system)
    private let replayButton = UIButton(type: .system)

    deinit {
        if let observeForComplete {
            NotificationCenter.default.removeObserver(observeForComplete)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        setupUI()
        setupPlayer()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        playerLayer?.frame = playerContainerView.bounds
    }

    // MARK: - Setup

    private func setupUI() {
        playerContainerView.translatesAutoresizingMaskIntoConstraints = false
        playerContainerView.backgroundColor = .black
        view.addSubview(playerContainerView)

        // Buttons
        playButton.setTitle("Play", for: .normal)
        stopButton.setTitle("Pause", for: .normal)
        replayButton.setTitle("RePlay", for: .normal)

        [playButton, stopButton, replayButton].forEach {
            $0.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
            $0.translatesAutoresizingMaskIntoConstraints = false
            $0.contentEdgeInsets = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
        }

        let stack = UIStackView(arrangedSubviews: [playButton, stopButton, replayButton])
        stack.axis = .horizontal
        stack.spacing = 16
        stack.alignment = .center
        stack.distribution = .equalCentering
        stack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stack)

        NSLayoutConstraint.activate([
            playerContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            playerContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            playerContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            playerContainerView.heightAnchor.constraint(equalToConstant: 200),

            stack.topAnchor.constraint(equalTo: playerContainerView.bottomAnchor, constant: 20),
            stack.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])

        // Action
        playButton.addTarget(self, action: #selector(didTapPlay), for: .touchUpInside)
        stopButton.addTarget(self, action: #selector(didTapStop), for: .touchUpInside)
        replayButton.addTarget(self, action: #selector(didTapReplayFromStart), for: .touchUpInside)
    }

    private func setupPlayer() {
        // AVURLAsset -> AVPlayerItem → AVPlayer
        let asset = AVURLAsset(url: videoURL)
        let item = AVPlayerItem(asset: asset)
        self.playerItem = item

        let player = AVPlayer(playerItem: item)
        player.automaticallyWaitsToMinimizeStalling = true
        self.player = player

        let layer = AVPlayerLayer(player: player)
        layer.videoGravity = .resizeAspect
        playerContainerView.layer.addSublayer(layer)
        layer.frame = playerContainerView.bounds
        self.playerLayer = layer

        // Notification
        if let observeForComplete {
            NotificationCenter.default.removeObserver(observeForComplete)
        }
        if let playerItem {
            observeForComplete = NotificationCenter.default.addObserver(
                forName: AVPlayerItem.didPlayToEndTimeNotification,
                object: playerItem,
                queue: .main
            ) { [weak self] _ in
                guard self != nil else { return }
                Task { @MainActor in
                    print("Play finished.")
                }
            }
        }
    }

    // MARK: - Actions

    @objc private func didTapPlay() {
        player?.play()
    }

    @objc private func didTapStop() {
        player?.pause()
    }
    
    // RePlay
    @objc private func didTapReplayFromStart() {
        player?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] _ in
            self?.player?.play()
        }
    }
}

I would greatly appreciate an official response from Apple engineering on whether this is an intentional change, a regression, or an API contract clarification, and what the recommended approach is going forward. Thank you.

The behavior of AVPlayerItem.didPlayToEndTimeNotification is not as expected in iOS 26.
 
 
Q