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()
...
}
I have been created page control, and in that the background videos will be get loaded, the videos is getting played but the loop is not working, i had refered in stackoverflow but i couldn't get the right answer
here is mine code:
override func viewDidLoad() {
super.viewDidLoad()
self.pageController.currentPage = 0
loadVideo(currentPage: 0)
}
func loadVideo(currentPage: Int) {
DispatchQueue.main.async {
self.videoPath = Bundle.main.url(forResource: "BoardVideoArray[currentPage]", withExtension: "mp4")
self.player = AVPlayer(url: self.videoPath!)
self.player?.actionAtItemEnd = .none
self.player?.isMuted = true
self.playerLayer = AVPlayerLayer(player: self.player)
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
self.playerLayer.zPosition = -1
}
playerLayer.frame = videoVIew.frame
videoVIew.layer.addSublayer(playerLayer)
videoVIew.bringSubview(toFront: pageController)
player?.play()
//loop video
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
self.player?.seek(to: kCMTimeZero)
self.player = AVPlayer(url: self.videoPath!)
self.player.play()
}
})
}
I have declared the player and avplayerLayer out of scope
var player: AVPlayer!
var playerLayer = AVPlayerLayer()
I spend more than 4 hours, but i couldn't found the mistake which i have made.
The issue is video is not geting looping
Try this way.
guard let path = Bundle.main.path(forResource: "video", ofType:"m4v") else {
debugPrint("video.m4v not found")
return
}
//VideoPlayer is a subClass of AVPlayer
let player = VideoPlayer(url: URL(fileURLWithPath: path))
playerViewController = AVPlayerViewController()
playerViewController.videoGravity = AVLayerVideoGravityResizeAspectFill
playerViewController.player = player
//self.videoPlayerView is a UIView in which i want to show video.
playerViewController.view.frame = self.videoPlayerView.bounds
self.videoPlayerView.addSubview(playerViewController.view)
playerViewController.showsPlaybackControls = false
playerViewController.player?.play()
player.shouldMute(mute: true)
NotificationCenter.default.addObserver(self, selector: #selector(HomeViewController.videoDidStopPlaying(notification:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
func videoDidStopPlaying(notification:Notification) {
self.playerViewController.player?.seek(to: kCMTimeZero)
self.playerViewController.player?.play()
}
I wrote a Swift UIView subclass called SDLoopingVideoView that plays and loops any compatible AVPlayer video file and automatically scales it to fill the view. It can be entirely setup in Interface Builder This work great as a video background and you won't have to worry about the video looping since it's managed automatically. You can find it on Github here: SDLoopingVideoView
I am working on an app where I need to play videos from a FOR loop i.e. play local video against the string provided in each iteration of the loop.
The problem is when I call the playVideo() func from the loop, all the videos play simultaneously. I want them to play one after the other and then dismiss the AVPlayerLayer i.e. remove from the superlayer.
When a single video is played, the playerLayer gets dismissed but in case of more than one, the playerLayer is intact.
How to make them play one after other?
I have read about using dispatch queues, but don't know much about them.
code :
func parseString(string: String){
var stringArray = string.componentsSeparatedByString(" ")
logTextView.text = ""
for i in 0..<stringArray.count{
playVideo(stringArray[i].lowercaseString)
}
}
var player: AVPlayer!
var playerLayer: AVPlayerLayer!
// video player
private func playVideo(name: String) {
guard let path = NSBundle.mainBundle().pathForResource(name, ofType:"mp4") else {
print("file not found")
return
}
player = AVPlayer(URL: NSURL(fileURLWithPath: path))
playerLayer = AVPlayerLayer(player: player)
// playerController.player = player
self.playerLayer.frame = self.view.bounds
self.view!.layer.addSublayer(self.playerLayer)
self.player!.play()
// playing = true
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.playerDidFinishPlaying(_:)),
name: AVPlayerItemDidPlayToEndTimeNotification, object: player.currentItem)
}
func playerDidFinishPlaying(note: NSNotification) {
print("Video Finished")
self.playerLayer.removeFromSuperlayer()
// playing = false
}
Any help would be appreciated
Don't use for loop then. Take an int i = 0, then increase (i++) until i becomes more than stringArray.count
if (i == stringArray.count) {
//end
}
call the function, where you are doing the above execution, from playerDidFinishPlaying(note: NSNotification).
Hope this will help.
I use following code to reveal two different video-sources as background. The "selectVideo" (SegmentedControl) is used to select video. The problem is down below.
#IBAction func selectVideo(sender: AnyObject) {
if self.Controller.selectedIndex == 1 {
self.videoBackgroundCustomer()
}
if self.Controller.selectedIndex == 0 {
self.videoBackgroundDriver()
}
}
func videoBackgroundDriver() {
//Load video background.
let videoURL: NSURL = NSBundle.mainBundle().URLForResource("background_video_2", withExtension: "mp4")!
player = AVPlayer(URL: videoURL)
videoBackground()
}
//Video background customer
func videoBackgroundCustomer() {
//Load video background.
let videoURL: NSURL = NSBundle.mainBundle().URLForResource("background_video_1", withExtension: "mp4")!
player = AVPlayer(URL: videoURL)
videoBackground()
}
//Vieobackground-code part 2, provides with less code.
func videoBackground() {
player?.actionAtItemEnd = .None
player?.muted = true
let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
playerLayer.zPosition = -1
playerLayer.frame = view.frame
view.layer.addSublayer(playerLayer)
player?.play()
//call loop video
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(LoginViewController.loopVideo), name: AVPlayerItemDidPlayToEndTimeNotification, object: player!.currentItem)
}
//Loop video
func loopVideo() {
player?.seekToTime(kCMTimeZero)
player?.play()
}
Problem: The video restarts when the last video should have ended. And not when the most recent video ends.
How do I make it repeat playing after the last playing video finished? Thanks
After investigation of the problem firstly take a look at documentation and return values of the method.
- (void)seekToTime:(CMTime)time;
Method that you are using return void and could not be compared to the CMTime
For solving of you're problem try this solution:
First you need to subscribe you're class to the notification that will indicate that video is ended.
NSNotificationCenter.defaultCenter().addObserver(self,selector: "itemDidReachEnd:",
name: AVPlayerItemDidPlayToEndTimeNotification,
object: player.currentItem)
And than define method to handle this notification.
func itemDidReachEnd(notification: NSNotification) {
player.seekToTime(kCMTimeZero)
player.play()
}
In this case you are tracking when video ends and you start it again.
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")
}