In my setup the user can tap a button to mute or unmute the AVPlayer. Simultaneously the video's subtitles are removed or added. But when the the AVPlayer is changed, for example from muted to unmuted, then the NotificationCenter does not inform me when AVPlayer is playing to end. I read somewhere that if AVPlayerItem is changed manually then I will not get any notification of AVPlayerItemDidPlayToEndTime. So I tried to remove the NotificationCenter's observer and add a new observer to it again. But it does not work
Here is my example
override func viewDidLoad() {
...
let player = AVPlayer(playerItem: playerItem)
player?.isMuted = true
if let group = asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristic.legible) {
let localeGER = Locale(identifier: "ger")
var options = AVMediaSelectionGroup.mediaSelectionOptions(from: group.options, with: localeGER)
if let option = options.first {
avoption = option
player?.currentItem?.select(option, in: group)
}
}
NotificationCenter.default.addObserver(self, selector: #selector(videoEnded),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
}
And here is the snippet when user tapped button
#objc private func disEnableSubAudio(_ sender: UIButton) {
isAudioOn = !isAudioOn
if isAudioOn == false {
let group = player!.currentItem!.asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristic.legible)
player?.currentItem?.select(nil, in: group!)
// unmute player
player?.isMuted = false
// check when video ended
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(videoEnded),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
} else {
if let group = player?.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristic.legible) {
let localeGER = Locale(identifier: "ger")
var options = AVMediaSelectionGroup.mediaSelectionOptions(from: group.options, with: localeGER)
if let option = options.first {
player?.currentItem?.select(option, in: group)
}
}
// mute player
player?.isMuted = true
}
}
AVPlayerItemDidPlayToEndTime only works when the AVPlayer or AVPlayerItem is not changed. So how can I check when the video is ending when some changes are made ?
There appears to be many solutions on SO addressing this yet none of those solutions have worked for me. I'm currently using Swift 5. I have a AVPlayer playing an animation (that loops) in my ViewController. When a call comes in through CallKit, regardless of whether I answer or decline the call, the animation played by the AVPlayer does not resume after the call has been dealt with. The interruption handler seems to be called before an interruption but usually doesn't get called after the interruption.
override func viewDidLoad() {
super.viewDidLoad()
prepareBGVideo()
...
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillEnterForeground(notification:)),
name: UIApplication.willEnterForegroundNotification,
object: nil)
...
}
func prepareBGVideo() {
guard let path = Bundle.main.path(forResource: "animation", ofType:"mp4") else {
print("video not found")
return
}
let item = AVPlayerItem(url: URL(fileURLWithPath: path))
avPlayer = AVPlayer(playerItem: item)
NotificationCenter.default.addObserver(self,
selector: #selector(loopVideoBG),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: item)
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption(notification:)), name: AVAudioSession.interruptionNotification, object: nil)
avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer.backgroundColor = UIColor.black.cgColor
avPlayer.volume = 0
avPlayer.actionAtItemEnd = .none
avPlayer.play()
view.backgroundColor = .clear
avPlayerLayer.frame = view.layer.bounds
view.layer.insertSublayer(avPlayerLayer, at: 0)
avPlayerLayer.videoGravity = isIPAD ? AVLayerVideoGravity.resize : AVLayerVideoGravity.resizeAspectFill // Changed from AVLayerVideoGravity.resizeAspect to AVLayerVideoGravity.resize so that video fits iPad screen
NotificationCenter.default.addObserver(self,
selector: #selector(willEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
#objc func handleInterruption(notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
// Interruption began, take appropriate actions (save state, update user interface)
self.avPlayer.pause()
} else if type == .ended {
guard let optionsValue =
info[AVAudioSessionInterruptionOptionKey] as? UInt else {
return
}
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
self.avPlayer.play()
}
}
}
/// Resume video while app wake up from background
#objc func willEnterForeground() {
avPlayer.seek(to: CMTime.zero)
JPUtility.shared.performOperation(0.1) {
self.avPlayer.play()
}
}
#objc func loopVideoBG() {
avPlayer.seek(to: CMTime.zero)
avPlayer.play()
}
Here are all the solutions that I have tried:
Waiting two seconds before calling self.avPlayer.play() in if options.contains(.shouldResume){}
Setting AVAudioSession.sharedInstance().setActive to false when interruption begins and then setting it ot true when interruption ends. The issue with this approach is that the if interruption == .ended {} block doesn't always get invoked so setting setActive had no effect.
Setting AVAudioSession playback category to AVAudioSessionCategoryOptions.MixWithOthers. My animation doesn't have audio anyway.
I have seen mentions of resuming playback in applicationDidBecomeActive(_:) but some advised against this. Would this be considered good practice?
Is there a way to ensure that the else if type == .ended {} block gets executed? Or perhaps a workaround that works more reliably than observing AVAudioSession.interruptionNotification?
I solved this but creating a shared VideoPlayer class that contained references to all the screen that had animations.
import Foundation
import UIKit
import AVKit
class VideoPlayer: NSObject {
static var shared: VideoPlayer = VideoPlayer()
var avPlayer: AVPlayer!
var avPlayerLayer: AVPlayerLayer!
weak var vcForConnect:ConnectVC?
weak var vcForList:ListVC?
override init() {
super.init()
guard let path = Bundle.main.path(forResource: "animation", ofType:"mp4") else {
print("video not found")
return
}
avPlayer = AVPlayer(url: URL(fileURLWithPath: path))
avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
avPlayer.volume = 0
avPlayer.actionAtItemEnd = .none
loopVideo(videoPlayer: avPlayer)
avPlayer.play()
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption(notification:)), name: AVAudioSession.interruptionNotification, object: nil)
}
deinit {
avPlayer.pause()
}
#objc func handleInterruption(notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
// Interruption began, take appropriate actions (save state, update user interface)
self.avPlayer.pause()
} else if type == .ended {
guard let optionsValue =
info[AVAudioSessionInterruptionOptionKey] as? UInt else {
return
}
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
self.avPlayer.play()
}
}
}
func resumeAllAnimations() {
self.avPlayer.play()
if vcForList?.avPlayer != nil {
vcForList?.avPlayer.play()
}
if vcForConnect?.avPlayer != nil {
vcForConnect?.avPlayer.play()
}
if vcForConnect?.avPlayerBG != nil {
vcForConnect?.avPlayerBG.play()
}
}
...
}
I then resume the animations by calling resumeAllAnimations() in applicationDidBecomeActive(_:) in AppDelegate.swift like so:
func applicationDidBecomeActive(_ application: UIApplication) {
VideoPlayer.shared.resumeAllAnimations()
...
}
As soon as the file has played, the app crashes and i get this error:
-[fefef.ViewController lavEnFunktionDerSkifterTitle(sender as! UIButton)]: unrecognized selector sent to instance 0x7f9883785f40
How can i solve it?
Here is my code:
#IBAction func play(sender: AnyObject) {
musikAfspiller("wwww.myurl.com")
lavEnFunktionDerSkifterTitle(sender as! UIButton)
}
func musikAfspiller (url: String) {
let playerItem = AVPlayerItem( URL:NSURL( string:url ) )
player = AVPlayer(playerItem:playerItem)
player.play()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"lavEnFunktionDerSkifterTitle()",name: AVPlayerItemDidPlayToEndTimeNotification, object: playerItem)
}
func lavEnFunktionDerSkifterTitle(sender: UIButton) {
if counter == 1 {
counter = 2
sender.setTitle("Stop", forState: UIControlState.Normal)
} else {
counter = 1
sender.setTitle("Play", forState: UIControlState.Normal)
}
}
the syntax for the observer declaration should be:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "lavEnFunktionDerSkifterTitle:", name: AVPlayerItemDidPlayToEndTimeNotification, object: playerItem)
and the receiving function should be outside the viewDidLoad() function in this syntax:
func lavEnFunktionDerSkifterTitle(notification: NSNotification) {}
The selector declaration should not have the parentheses when you sending the notification use this:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"lavEnFunktionDerSkifterTitle",name: AVPlayerItemDidPlayToEndTimeNotification, object: playerItem)
Also the signeture of the function receiving the notification should look like this:
func lavEnFunktionDerSkifterTitle(notification: NSNotification)
More details about NSNotification can be found here
I am using the MPMoviePlayerController to play a video. I want to dismiss the video when it has finished playing. Here is my code:
import UIKit
import MediaPlayer
class programViewController: UIViewController {
var moviePlayer : MPMoviePlayerController?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
playVideo("video", type: "mov")
}
func playVideo(navn:String, type:String) {
let path = NSBundle.mainBundle().pathForResource(navn, ofType: type)
let url = NSURL.fileURLWithPath(path!)
moviePlayer = MPMoviePlayerController(contentURL: url)
if let player = moviePlayer {
player.view.frame = self.view.bounds
player.prepareToPlay()
player.scalingMode = .AspectFit
player.shouldAutoplay = true
player.fullscreen = false
self.view.addSubview(player.view)
}
}
}
You should add in playVideo function
NSNotificationCenter.defaultCenter().addObserver(self, selector: "movieFinishedCallback:", name: MPMoviePlayerPlaybackDidFinishNotification, object: player)
and in your classs:
func movieFinishedCallback(notif:NSNotification) {
// Obtain the reason why the movie playback finished
var userInfo:Dictionary<String,Int!> = notif.userInfo as! Dictionary<String,Int!>
let finishReason : Int = userInfo[MPMoviePlayerPlaybackDidFinishReasonUserInfoKey]!
// Dismiss the view controller ONLY when the reason is not "playback ended"
if ( finishReason == MPMovieFinishReason.PlaybackEnded.rawValue)
{
let moviePlayer:MPMoviePlayerController = notif.object as! MPMoviePlayerController
// Remove this class from the observers
NSNotificationCenter.defaultCenter().removeObserver(self, name: MPMoviePlayerPlaybackDidFinishNotification, object: self.moviePlayerController!.moviePlayer)
// Dismiss the view controller
moviePlayer.view.removeFromSuperview()
}
}
updated version of #roman-barzyczak's answer:
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(movieFinishedCallback(_:)), name: MPMoviePlayerPlaybackDidFinishNotification, object: moviePlayer)
then:
func movieFinishedCallback(notification: NSNotification) {
if let userInfo = notification.userInfo as? [String : NSNumber] {
let reason = userInfo[MPMoviePlayerPlaybackDidFinishReasonUserInfoKey]
let finishReason = MPMovieFinishReason(rawValue: reason!.integerValue)
if (finishReason != MPMovieFinishReason.PlaybackEnded),
let moviePlayer = notification.object as? MPMoviePlayerController {
NSNotificationCenter.defaultCenter().removeObserver(self, name: MPMoviePlayerPlaybackDidFinishNotification, object: moviePlayer)
moviePlayer.view.removeFromSuperview()
}
}
}
You would need to listen for MPMoviePlayerPlaybackDidFinishNotification notification.
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("playerPlaybackDidFinish"), name: MPMoviePlayerPlaybackDidFinishNotification, object: nil)
// Present
self.presentViewController(moviePlayer, animated: ture, completion: nil)
// Dismiss
moviewPlayer.dismissViewControllerAnimated(true, completion: nil)
It is possible just to delete the view from subviews, and because the last made subview is the movie player view you can delete it just by using this code:
self.view.subviews[self.view.subviews.count-1].removeFromSuperview()
I'am using AVPlayer for playing local video file (mp4) in Swift.
Does anyone know how to detect when video finish with playing?
Thanks
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
)
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")
}
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)
}
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")
}
}
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")
}
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)
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!")
}
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")
}
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
}
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")
}
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
}
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
}
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)
}
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)
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: nil, queue: .main) { noti in
guard let item = noti.object as? AVPlayerItem else{
return
}
//DidPlayToEndTime
}
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)
}
}
}
Another solution:
player.observe(\AVPlayer.actionAtItemEnd) { player, _ in
print("video did end")
}