0

I've a SwiftUi app with a View in which I want to play a sequence of videos from remote. I've used an AVQueuePlayer.

At the end of any reproduction I want to play the next video after a defined pause from an array of pauses.

This code work only after the first played item, and then the observer not intercept nothing. Any suggestion?

struct ExercisesPlay: View {
    
    @State var urls: [URL]
    @State var pauses: [Int]
    
    var player: AVQueuePlayer
    
    @State var showToast: Bool = false
    
    init(urls: [URL], pauses: [Int]) {
        self.urls = urls
        self.pauses = pauses
        
        var array: [AVPlayerItem] = []
        urls.forEach { URL in
            array.append(AVPlayerItem(url: URL))
        }
        self.player = AVQueuePlayer(items: array)
    }
    
    
    var body: some View {
        VideoPlayer(player:player)
            .onAppear{
                player.play()
                addObserver()
            }
            .onDisappear{
                removeObserver()
            }
            .toast(message: LocalizedStringKey("Pause").stringValue()!,
                   isShowing: self.$showToast, duration: Toast.long)
        
    }
    
    func addObserver() {
        NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue:nil ) { notif in

            self.showToast.toggle()
            
            if(!pauses.isEmpty){
                
                print(player.currentItem)
                
                player.pause()
                
                DispatchQueue.main.asyncAfter(deadline: (.now() + DispatchTimeInterval.seconds(pauses.first!))) {
                    player.seek(to: .zero)
                    player.play()
                    pauses.remove(at: 0)
                    
                }
            }
        }
    }
    
    func removeObserver() {
        NotificationCenter.default.removeObserver(self,name: .AVPlayerItemDidPlayToEndTime, object: nil)
    }
    
    
}
Lamoichenzio
  • 75
  • 1
  • 1
  • 7
  • `init` may be called many times. It's not a good idea to do heavy work (like setting up the player) in `init`. Also, if `init` *does* get called again, your `player`, which is not in a `@State` variable, will get recreated on each run. – jnpdx Mar 17 '23 at 15:40
  • @jnpdx it is exactly what I want, every time it will have to restart again. – Lamoichenzio Mar 18 '23 at 10:47

2 Answers2

1

I think this might be because you're only setting the observer on the currentItem. I would need to investigate myself if it's possible to put it on all items without removing and adding the observer over and over again.

You could set listen to every change of the currentItem. Then everytime an item is changed, you could pause the player for x amount of seconds and start the player again.

let cancellables = Set<AnyCancellable>()

player.publisher(for: \.currentItem).sink(receiveValue: { _ in
   print("New currentItem")
}).store(in: &cancellables)

Hope this helps.

adams.s
  • 185
  • 1
  • 14
  • I tried this approach, it works at the end of the first video and print "Item ended" but I can't perform any mutation on the player like advanceToNextItem() or play(). – Lamoichenzio Mar 18 '23 at 10:46
  • @adam.s have any suggestions about how use this observer? – Lamoichenzio Mar 18 '23 at 10:48
  • Hi @Lamoichenzio, my apologies, the previous answer, listening on actionAtItemEnd won't work, because it's only triggered when the value is set. (Which is done manually). I've updated my answer to a solution that should work better in this case. Although the answer below of Matthew will also work and might be the better solution. This will actually be triggered on every end of an item, while my solution will be triggered on every change of the item in the queue (which will not happen with the last item for example.) – adams.s Mar 19 '23 at 18:28
1

The observer is only being added to the current item when the view appears.

You can add an observer to each item before passing them to the AVQueuePlayer. If you include a reference to the object, you can more easily remove the observer later.

urls.forEach { URL in
    let item = AVPlayerItem(url: URL)
    NotificationCenter.default.addObserver(self, selector: #selector(itemFinishedPlaying), name: .AVPlayerItemDidPlayToEndTime, object: item)
    array.append(item)
}

In the selector method, you can do your setup and cleanup the observer.

@objc private func itemFinishedPlaying(_ notification: NSNotification) {
NotificationCenter.default.removeObserver(self, name: notification.name, object: notification.object)

// additional code you want to perform goes here

}

If you want to cleanup all of the observers when the view disappears, you can iterate through the queued items.

for item in player.items() {
    NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: item)
}
matthew
  • 26
  • 2