Avplayer AVPlayerItemDidPlayToEndTime notificaton not sending message to observer

3.1k Views Asked by At

I am using AVQueuePlayer to play a list of remote audio files. I want to implement repeat all by default.

My approach, I am observing AVPlayerItemDidPlayToEndTime notification and I add the playerItem to the back of the Queue when it has finished playing.

The nextAudio(notification: Notification) is not running at all. Need help on this or better still a better way to implement infinite play.

func playAudio(_ items: [AVPlayerItem]) {

    let avPlayerVC = AVPlayerViewController()
    let player = AVQueuePlayer(items: items)
    player.actionAtItemEnd = .pause
    avPlayerVC.player = player

    for item in items {
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(FilesViewController.nextAudio(notification:)),
                                               name: .AVPlayerItemDidPlayToEndTime, object: item)
    }

    present(avPlayerVC, animated: true) {
        self.player.play()
    }
}

@objc func nextAudio(notification: Notification) {
    debugPrint("nextAudio was called")
    guard player != nil else { return }
    debugPrint("AVPlayerItemDidPlayToEndTime notif info  \(notification.userInfo)")
    if let currentItem = notification.userInfo!["object"] as? AVPlayerItem {
        currentItem.seek(to: kCMTimeZero)
        self.player.advanceToNextItem()
        self.player.insert(currentItem, after: nil)
    }
}
2

There are 2 best solutions below

0
Lukasz On

I am sure you already figured it out but I just encountered myself and decided to answer anyways:

It looks like it is not delivered when you specify an object of notification (which should be a proper way BTW). may be a bug in iOS...

You need to pass nil instead:

NotificationCenter.default.addObserver(self, selector: #selector(videoDidPlayToEnd), name: .AVPlayerItemDidPlayToEndTime, object: nil)

Then in the method you should check in the notification's object is your player item. Documentation is actually not consistent because Apple states that notification's object is a AVplayer but it is a AVPlayerItem:

@objc
    private func videoDidPlayToEnd(_ notification: Notification) {
        guard let playerItem = notification.object as? AVPlayerItem, let urlAsset = playerItem.asset as? AVURLAsset else { return }
    gpLog("Sender urlAsset: \(urlAsset.url.absoluteString)")
        // Compare an asset URL.
    }
0
Chase McElroy On

Thank you @Lukasz for your great answer! I was having an issue with multiple videos firing the notification at the wrong time. Your answer helped me fix it.

If anyone is looking for examples of how to use this with SwiftUI here's my code below:

First create a player:

import SwiftUI
import AVKit

struct VideoPlayer : UIViewControllerRepresentable {

func makeCoordinator() -> VideoPlayer.Coordinator {
    return VideoPlayer.Coordinator(parent1: self)
}

@Binding var didFinishVideo : Bool
@Binding var player : AVPlayer
var play: Bool
var loop: Bool
var videoName: String
var controller = AVPlayerViewController()

func makeUIViewController(context: UIViewControllerRepresentableContext<VideoPlayer>) -> AVPlayerViewController {
    controller.player = player
    controller.showsPlaybackControls = false
    controller.videoGravity = .resize
    NotificationCenter.default.addObserver(context.coordinator, selector: #selector(context.coordinator.playerDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: nil)
    return controller
}

func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
    if play {
        player.play()
    }
}

class Coordinator : NSObject{
    var parent : VideoPlayer
    init(parent1 : VideoPlayer) {
        parent = parent1
    }

    @objc func playerDidFinishPlaying(_ notification: Notification) {
        guard let playerItem = notification.object as? AVPlayerItem, let urlAsset = playerItem.asset as? AVURLAsset else { return }
        print("Sender urlAsset: \(urlAsset.url.absoluteString)")

        if urlAsset.url.absoluteString.contains(parent.videoName) {
            if parent.loop {
                parent.player.seek(to: CMTime.zero)
                parent.player.play()
            } else {
                parent.didFinishVideo = true
            }
        }

    }

}
}

The you can use it to create multiple videos like this:

import SwiftUI
import AVKit

struct ExampleVideo: View {
    @Binding var didFinishVideo : Bool
    var play: Bool
    @State private var player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "exampleVideoFileName", ofType: "mp4")!))
    var body: some View {
        VideoPlayer(didFinishVideo: $didFinishVideo, player: $player, play: play, loop: false, videoName: "exampleVideoFileName")
    }
}

struct ExampleVideo_Previews: PreviewProvider {
    static var previews: some View {
        CeveraIntroVideo(didFinishVideo: .constant(true), play: true)
    }
}

Here's an example of using it in a view after something loads:

struct IntroScreens: View {

@State var loadingComplete = false
@State var didFinishVideo = false

var body: some View {
        ZStack{
            ExampleVideo(didFinishVideo: $didFinishVideo, play: loadingComplete)
                .zIndex(loadingComplete ? 3 : 0)
                .animation(Animation.easeInOut(duration: 1))
        }
    }
}