103

I'am using AVPlayer for playing local video file (mp4) in Swift. Does anyone know how to detect when video finish with playing? Thanks

Nik
  • 1,517
  • 5
  • 14
  • 19
  • ObjC example but you can go with the same way: http://stackoverflow.com/questions/6837002/no-avplayer-delegate-how-to-track-when-song-finished-playing-objective-c-iphon – kabarga Apr 01 '15 at 10:16

17 Answers17

134

To get the AVPlayerItemDidPlayToEndTimeNotification your object needs to be an AVPlayerItem.

To do so, just use the .currentItem property on your AVPlayer

Now you will get a notification once the video ends!

See my example:

let videoPlayer = AVPlayer(URL: url)       

NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerDidFinishPlaying:",
        name: AVPlayerItemDidPlayToEndTimeNotification, object: videoPlayer.currentItem)

func playerDidFinishPlaying(note: NSNotification) {
    print("Video Finished")
}

Swift 3

let videoPlayer = AVPlayer(URL: url)       

NotificationCenter.default.addObserver(self, selector: Selector(("playerDidFinishPlaying:")), 
       name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: videoPlayer.currentItem)
    
func playerDidFinishPlaying(note: NSNotification) {
    print("Video Finished")
}

Don't forget to remove the Observer in your deinit

Swift 4, 5

NotificationCenter.default
    .addObserver(self,
    selector: #selector(playerDidFinishPlaying),
    name: .AVPlayerItemDidPlayToEndTime,
    object: videoPlayer.currentItem
)
Zev Eisenberg
  • 8,080
  • 5
  • 38
  • 82
jstn
  • 1,862
  • 1
  • 13
  • 7
  • do you know what the difference between using this method and using the AVPlayer method `addPeriodicTimeObserver(TimeInterval:)` ? – MikeG Oct 11 '17 at 19:39
  • addPeriodicTimeObeserver is used when you want to be notified when the time interval is up. for example, if you want to be notified every 0.5 second, you'd create a CMTime of 0.5 seconds and the callback would be invoked every 0.5 seconds. – jstn Oct 11 '17 at 19:48
  • does this addObserver induce memory leak when used multiple times? – MartianMartian Oct 16 '17 at 13:43
  • 3
    how to remove it please – MartianMartian Oct 16 '17 at 13:44
  • 4
    From https://developer.apple.com/library/archive/releasenotes/Foundation/RN-FoundationOlderNotes/index.html#10_11NotificationCenter In OS X 10.11 and iOS 9.0 NSNotificationCenter and NSDistributedNotificationCenter will no longer send notifications to registered observers that may be deallocated. – Joshua D. Boyd Aug 02 '18 at 22:50
  • 1
    Passing the currentItem for the `object` Parameter was the important part - check that if you have problems. – Jan Erik Schlorf Jul 16 '19 at 11:38
  • 2
    wrong on two counts (1) you actually don't have to pass anything as an object (2) for years now, no need to deinit notifications – Fattie Sep 17 '19 at 21:20
  • Can I use the function didFinishPlaying to reset the seekTime to zero? If so, how do I do that? – thenakulchawla Apr 19 '20 at 06:53
  • I had added `NotificationCenter.default.addObserver...` and declared @objc in my Swift class but the @objc is never called. I am using Xcode 13 – MrinmoyMk Jun 26 '21 at 19:39
40

Swift 3.0

let videoPlayer = AVPlayer(URL: url)

NotificationCenter.default.addObserver(self, selector:#selector(self.playerDidFinishPlaying(note:)),name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)

@objc func playerDidFinishPlaying(note: NSNotification){
        print("Video Finished")
    }
Graham Perks
  • 23,007
  • 8
  • 61
  • 83
Channel
  • 2,183
  • 21
  • 16
17

Swift 4.2 Version:

var player: AVPlayer!
  //
  //
// Configure Player
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    let filepath: String? = Bundle.main.path(forResource: "selectedFileName", ofType: "mp4")
    if let filepath = filepath {
        let fileURL = URL.init(fileURLWithPath: filepath)
        player = AVPlayer(url: fileURL)
        let playerLayer = AVPlayerLayer(player: player)
        // Register for notification
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(playerItemDidReachEnd),
                                                         name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
                                                         object: nil) // Add observer

        playerLayer.frame = self.view.bounds
        self.view.layer.addSublayer(playerLayer)
        player.play()
    }
}
// Notification Handling
@objc func playerItemDidReachEnd(notification: NSNotification) {
    player.seek(to: CMTime.zero)
    player.play()
}
// Remove Observer
deinit {
    NotificationCenter.default.removeObserver(self)
}
Nish
  • 545
  • 5
  • 7
10

If you fancy using Combine:

private var cancelBag: Set<AnyCancellable> = []

NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)
.sink { _ in
    player.seek(to: CMTime.zero)
    player.play()
}
.store(in: &cancelBag)
dchakarov
  • 9,048
  • 3
  • 25
  • 20
8

For SWIFT 3.0 This is working fine

class PlayVideoViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PlayVideoViewController.finishVideo), name: NSNotification.Name.AVPlayerItemDidPlayToEndTimeNotification, object: nil)
    }

    func finishVideo()
    {
        print("Video Finished")
    }
}
CodeOverRide
  • 4,431
  • 43
  • 36
manvendra singh
  • 296
  • 3
  • 9
8

Swift 4.0

This one works for me. Thanks to @Channel

    private func playVideo(fileURL: String) {

            // Create RUL object
            let url = URL(string: fileURL)

            // Create Player Item object
            let playerItem: AVPlayerItem = AVPlayerItem(url: url!)
            // Assign Item to Player
            let player = AVPlayer(playerItem: playerItem)

            // Prepare AVPlayerViewController
            let videoPlayer = AVPlayerViewController()
            // Assign Video to AVPlayerViewController
            videoPlayer.player = player

            NotificationCenter.default.addObserver(self, selector: #selector(myViewController.finishVideo), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)

            // Present the AVPlayerViewController
            present(videoPlayer, animated: true, completion: {
                    // Play the Video
                    player.play()
            })

    }

    @objc func finishVideo()
    {
            print("Video Finished")
    }
Arash HF
  • 837
  • 12
  • 10
7

SWIFT 5 Update

The observer method with @objc function is not native. It is better to use event publisher in swift 5. Very simple.

Declare the following in the struct:

var pub = NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)

Then on any view, add

.onReceive(pub) { (output) in
    print("Video Finished")
}
Jack LIU
  • 71
  • 1
  • 2
6

2019

It's really this simple

NotificationCenter.default.addObserver(
    self,
    selector: #selector(fileComplete),
    name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
    object: nil
)

(It's fine for the object to be nil.)

and then

@objc func fileComplete() {
    print("IT'S DONE!")
}
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
Fattie
  • 27,874
  • 70
  • 431
  • 719
4

Swift 3.0

let videoPlayer = AVPlayer(URL: url)
NotificationCenter.default.addObserver(self, selector:#selector(self.playerDidFinishPlaying(note:)),name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)

func playerDidFinishPlaying(note: NSNotification){
    //Called when player finished playing
}
Jayaraj
  • 342
  • 3
  • 13
3

For SWIFT 3.0

Here 'fullUrl' is the URL of the video and make sure that there would be no space in the URL, You should replace 'Space' with '%20' so that URL will work file.

  let videoURL = NSURL(string: fullUrl)
  let player = AVPlayer(url: videoURL! as URL)

  playerViewController.delegate = self
  playerViewController.player = player
  self.present(playerViewController, animated: false) {

    self.playerViewController.player!.play()

    NotificationCenter.default.addObserver(self, selector: #selector(yourViewControllerName.playerDidFinishPlaying), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem)
  }

Add this below given method in your view controller.

func playerDidFinishPlaying(){    
print("Video Finished playing in style")
}
Anil shukla
  • 277
  • 3
  • 10
3

I know there are a lot of accepted answers here...

But, another route might be to add a boundary time observer to your AVPlayer. You would have to have the duration of the video, which you can get from your player.currentItem, and then add it as your desired time boundary.

fileprivate var videoEndObserver: Any?

func addVideoEndObserver() {
    guard let player = YOUR_VIDEO_PLAYER else { return }

    // This is just in case you are loading a video from a URL.
    guard let duration = player.currentItem?.duration, duration.value != 0 else {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { [weak self] in
            self?.addVideoEndObserver()
        })

        return
    }

    let endTime = NSValue(time: duration - CMTimeMakeWithSeconds(0.1, duration.timescale))
    videoEndObserver = player.addBoundaryTimeObserver(forTimes: [endTime], queue: .main, using: {
        self.removeVideoEndObserver()

        // DO YOUR STUFF HERE...
    })
}

func removeVideoEndObserver() {
    guard let observer = videoEndObserver else { return }

    videoPlayer.player?.removeTimeObserver(observer)
    videoEndObserver = nil
}
Designerd
  • 531
  • 5
  • 8
  • This is a fine idea and in practice it's often better to do this. With long material (songs etc) it's often better to catch it a second before the end (or less if you can), rather than deal with the total (undocumented) Apple mess when a file actually "ends". – Fattie Sep 17 '19 at 21:23
3
func shareEditedVedio() -> AVPlayer {
    let editedVedioPlayer = AVPlayer(url: self.vedioData.vedioURLWithAddedSounds!)
    NotificationCenter.default.addObserver(self, selector:#selector(self.playerDidFinishPlaying(note:)),name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: editedVedioPlayer.currentItem)
    return editedVedioPlayer
}


@objc func playerDidFinishPlaying(note: NSNotification){
    //Called when player finished playing
}
2

In Swift 3 and RxSwift 3.5 all you have to do is:

override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter.default.rx.notification(Notification.Name.AVPlayerItemDidPlayToEndTime)
        .asObservable().subscribe(onNext: { [weak self] notification in

            //Your action

        }).addDisposableTo(disposeBag)
}
mkkrolik
  • 1,200
  • 14
  • 21
1

Using Combine, and also making sure the notification comes from the AVPlayerItem you are interested in and not just any. I am playing multiple items at once, so this would work in that scenario as well.

private var subscriptions: Set<AnyCancellable> = []

NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
    .receive(on: RunLoop.main)
    .sink { [weak self] notification in
        guard let item = notification.object as? AVPlayerItem else { return }
        if item == self?.player.currentItem {
         //.... Here you know it was the item you are interested in that played to end and not just any
        }
    }
    .store(in: &subscriptions)
zumzum
  • 17,984
  • 26
  • 111
  • 172
1
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: nil, queue: .main) { noti in
        guard let item = noti.object as? AVPlayerItem else{
            return
        }
        //DidPlayToEndTime
    }
Mike
  • 139
  • 1
  • 4
0

I had an issue with the Notification never getting called, setting the notification inside the presentation of the AVPlayerViewController solved it for me:

func presentVideo(url:URL) {
        let player = AVPlayer(url: url)
        let playerViewController = AVPlayerViewController()
        playerViewController.player = player
        
        self.present(playerViewController, animated: true) {
            DispatchQueue.main.async {
                playerViewController.player!.play()
                //NOTE: The notification must be created here for it to work as expected
                NotificationCenter.default.addObserver(self, selector: #selector(self.videoDidEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
            }
        }
    }
Buyin Brian
  • 2,781
  • 2
  • 28
  • 48
0

Another solution:

player.observe(\AVPlayer.actionAtItemEnd) { player, _ in
    print("video did end")
}
Maor
  • 3,340
  • 3
  • 29
  • 38