Resuming AVPlayer after phone call - ios

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()
...
}

Related

AVPlayer doesn't play video until a video from UIImagePickerController is played

I'm having a problem which I haven't seen a post like it on here. I have an AVPlayerViewController which plays a video-based off the path from my Firebase Database (not Storage). The video plays perfectly as I want it to, but only once I watch a video that is clicked on in the UIImagePickerController elsewhere in the app.
So for example, the AVPlayer will show a black background (this occurs with all of the AVPlayer's in the app), except for when I watch a video from the UIImagePickerController which has nothing to do with any of the other views. I have no clue where to start with this. I appreciate all your help and suggestions!
Here is the example code of my AVPlayerViewController:
import UIKit
import AVFoundation
import AVKit
class VideoView: UIViewController {
private var videoURL: URL
var player: AVPlayer?
var playerController : AVPlayerViewController?
init(videoURL: URL) {
self.videoURL = videoURL
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.gray
player = AVPlayer(url: videoURL)
playerController = AVPlayerViewController()
guard player != nil && playerController != nil else {
return
}
playerController!.showsPlaybackControls = false
playerController!.player = player!
self.addChild(playerController!)
self.view.addSubview(playerController!.view)
playerController!.view.frame = view.frame
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player!.currentItem)
let cancelButton = UIButton(frame: CGRect(x: 10.0, y: 10.0, width: 30.0, height: 30.0))
cancelButton.setImage(#imageLiteral(resourceName: "cancel"), for: UIControl.State())
cancelButton.addTarget(self, action: #selector(cancel), for: .touchUpInside)
view.addSubview(cancelButton)
// Allow background audio to continue to play
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.ambient)
} catch let error as NSError {
print(error)
}
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch let error as NSError {
print(error)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
player?.play()
}
#objc func cancel() {
dismiss(animated: true, completion: nil)
}
#objc fileprivate func playerItemDidReachEnd(_ notification: Notification) {
if self.player != nil {
self.player!.seek(to: CMTime.zero)
self.player!.play()
}
}
}
Did you check the size of child controller view?
playerController!.view.frame = view.frame
Put it into
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
playerController!.view.frame = view.frame
}
or use constraints.
The problem is that when your view appears it might not be ready for playback. What you should do to properly handle this is to KVO on the player to make sure it is ready to play.
I have answered this before here for a similar question:
Video playback issues only on iOS 13 with AVPlayerViewController and AVPlayer when using HLS video
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new],
context: &playerItemContext)
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// Only handle observations for the playerItemContext
guard context == &playerItemContext else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItemStatus
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
// Switch over status value
switch status {
case .readyToPlay:
// Player item is ready to play.
case .failed:
// Player item failed. See error.
case .unknown:
// Player item is not yet ready.
}
}
}
You can find documentation about it from Apple here: https://developer.apple.com/documentation/avfoundation/media_assets_playback_and_editing/responding_to_playback_state_changes
Another example of usage is here:
playerItem?.observe(\AVPlayerItem.status, options: [.new, .initial]) { [weak self] item, _ in
guard let self = self else { return }
switch status {
case .readyToPlay:
// Player item is ready to play.
case .failed:
// Player item failed. See error.
case .unknown:
// Player item is not yet ready.
}

Problem when attempting to loop AVPlayer (userCapturedVideo) seamlessly

I have been looking around for a while on how to correctly accomplish this. I have looked here and here. And have used the top answer here, to try and accomplish this however for me the recorded video does not ever even begin to loop. The first frame shows up but does not play the video, thus I am wondering what's wrong.
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
if (error != nil) {
print("Error recording movie11: \(error!.localizedDescription)")
} else {
isSettingThumbnail = false
let videoRecorded = outputURL! as URL
playRecordedVideo(video: videoRecorded)
if !captureSession.isRunning {
DispatchQueue.global(qos: .background).async {
self.startRunningCaptureSession()
}
}
}
}
The function used in the above method is bellow.
func playRecordedVideo(video: URL) {
thumbImage = nil
playerQueue = AVQueuePlayer(playerItem: AVPlayerItem(url: video))
playerLayer = AVPlayerLayer(player: playerQueue)
playerLayer.frame = (camBaseLayer?.bounds)!
playerLayer?.layoutIfNeeded()
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer.isHidden = false
camBaseLayer?.layer.insertSublayer(playerLayer, above: previewLayer)
playerItem1 = AVPlayerItem(url: video)
playerLooper = AVPlayerLooper(player: playerQueue, templateItem: playerItem1)
self.playerQueue?.play()
}
I have attempted the following in the function above:
var num = 0
if num == 0 {
self.playerQueue?.play()
num+=1
} else {
NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.playerQueue.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
self.playerQueue.seek(to: CMTime.zero)
self.playerQueue.play()
}
})
}
This however just causes the video to not even play when it comes up.
The goal is to prevent the "gap" you see when the video is looped
Update:
Ok so actually the variable num should be outside of the function which then would make the NS code run but that ends up freezing the view like it did before.
Update 2:
I have not been able to find a solution to this as of yet. But what is for certain is that
` NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.playerQueue.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
self.playerQueue.seek(to: CMTime.zero)
self.playerQueue.play()
}
})
`
Causes the video to not even loop. Any reason as to why this happens?
I did the same thing a few years back using this piece of code:
var player: AVPlayer!
var playerLayer: AVPlayerLayer!
private func playVideo(name: String) {
guard let path = Bundle.main.path(forResource: name, ofType:"mp4") else { return }
player = AVPlayer(url: NSURL(fileURLWithPath: path) as URL)
playerLayer = AVPlayerLayer(player: player)
self.playerLayer.frame = SOME_BOUNDS
self.player!.play()
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(note:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
}
#objc func playerDidFinishPlaying(note: NSNotification) {
print("Video Finished")
self.playerLayer.removeFromSuperlayer()
playVideo(name: "SOME_NAME")
}
Hope this points you to your desired functionality

MPNowPlayingInfoCenter not showing details

today I spent hours trying to figure out why MPNowPlayingInfoCenter is not working, but without success. I want it to show info in control center and lockscreen, the media is a video.
Here is the problem:
I have a singleton class called GlobalAVPlayer that holds an AVPlayerViewController. It is a singleton because there must be only one, and I need to access it globally.
class GlobalAVPlayer: NSObject {
static let sharedInstance = GlobalAVPlayer()
private var _currentVideo: Video?
var playerViewController = AVPlayerViewController()
var isPlaying: Bool = false
var almostPlaying: Bool = false
var hasItemToPlay: Bool = false
var currentVideo: Video?
{
set {
_currentVideo = newValue
notify_VideoChanged()
}
get {
return _currentVideo
}
}
private var player: AVPlayer!
override init()
{
super.init()
player = AVPlayer()
playerViewController.player = player
player.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.New, context: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(didPlayToEnd), name: "AVPlayerItemDidPlayToEndTimeNotification", object: nil)
}
func itemToPlay(item: AVPlayerItem)
{
if let player = player {
almostPlaying = true
hasItemToPlay = true
player.replaceCurrentItemWithPlayerItem(item)
}
}
func didPlayToEnd()
{
print("[GlobalAVPlayer] End video notification")
let time = CMTimeMakeWithSeconds(0, 1)
player.seekToTime(time)
}
func play()
{
if player.rate == 0
{
player.play()
if player.rate != 0 && player.error == nil
{
isPlaying = true
print("[GlobalAVPlayer] Playing video without errors")
}
}
}
func pause()
{
if player.rate == 1
{
player.pause()
if player.rate == 0 && player.error == nil
{
isPlaying = false
print("[GlobalAVPlayer] Pausing video without errors")
}
}
}
func notify_PlaybackChanged()
{
NSNotificationCenter.defaultCenter().postNotificationName("globalAVPlayerPlaybackChanged", object: self)
}
func notify_VideoChanged()
{
NSNotificationCenter.defaultCenter().postNotificationName("globalAVPlayerVideoChanged", object: self)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "rate" {
let newRate = change!["new"] as! Int
//rate = 0 (il player รจ fermo) rate = 1 (il player sta andando)
self.isPlaying = newRate == 1 ? true : false
notify_PlaybackChanged()
}
}
deinit
{
player.removeObserver(self, forKeyPath: "rate", context: nil)
}
}
The init is called one time when app starts, after that I use "itemToPlay" method to change the video.
I have also correctly (I think) configured the audio session in AppDelegate:
do
{
let session = AVAudioSession.sharedInstance()
try session.setCategory(AVAudioSessionCategoryPlayback)
try session.setActive(true)
}
catch
{
print("[AppDelegate] Something went wrong")
}
I tried to put MPNowPlayingInfoCenter.defaultCenter().nowPlayingInfo = something everywhere, but at max I could see the video title in the control center for 1 second when I put it into the play() method. After that it disappeared and the playback controls went gray. Information is correctly stored, because when i print nowPlayingInfo content into the console everything is as expected.
I tried using becomeFirstResponder and UIApplication.beginReceivingRemoteControlEvents in different places without success.
Thanks
Starting with iOS 10 there is a BOOL property in AVPlayerViewController called updatesNowPlayingInfoCenter, that has the default value: YES. Just change it to NO:
//playerController is an instance of AVPlayerViewController
if ([self.playerController respondsToSelector:#selector(setUpdatesNowPlayingInfoCenter:)])
{
self.playerController.updatesNowPlayingInfoCenter = NO;
}

Play audio while downloading without downloading file twice?

I would like to write an iOS app that plays an audio file as soon as there is enough data while continuing to download. It seems that you can either download and play when the download is finishing (Using NSURLProtocol to implement play while downloading for AVPlayer on iOS) or continuously stream without getting to save a file (Playing audio file while I download it). Is there any way to download and play at the same time without downloading two copies of the file?
You can do that with the help of AVAssetResourceLoader and AVPlayer
Here is the link of the tutorial
http://leshkoapps.com/wordpress/audio-streaming-and-caching-in-ios-using-avassetresourceloader-and-avplayer/
And here is the Github repo for that
https://github.com/leshkoapps/AVAssetResourceLoader
here you can use below functions without download (Streaming)
import AVFoundation
var progressTimer:Timer?
{
willSet {
progressTimer?.invalidate()
}
}
var playerStream: AVPlayer?
var playerItem: AVPlayerItem?
func playerStream(urlStream : String)
{
if let playerStream = playerStream
{
if playerStream.isPlaying
{
stopProgressTimer()
playerStream.pause()
}
else
{
startProgressTimer()
playerStream.play()
}
}
else
{
if let urlStr = urlStream.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
{
if let TempURL = URL.init(string: urlStr)
{
playerItem = AVPlayerItem(url: TempURL)
playerStream = AVPlayer(playerItem: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidPlayToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem)
}
}
}
}
func playerItemDidPlayToEndTime() {
stopProgressTimer()
self.playProgressView.progress = 0.0
if let playerStream = self.playerStream
{
playerStream.replaceCurrentItem(with: playerItem)
playerStream.seek(to: kCMTimeZero)
// playerStream.seek(to: .zero) swift 4.0
}
}
func stopProgressTimer() {
progressTimer?.invalidate()
progressTimer = nil
}
func startProgressTimer()
{
if #available(iOS 10.0, *) {
progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true){_ in
self.updateProgressTimer()
}
}
else {
progressTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateProgressTimer), userInfo: nil, repeats: true)
}
}
#objc func updateProgressTimer()
{
if let playerItem = playerItem
{
if let pa = playerStream
{
let floatTime = Float(CMTimeGetSeconds(pa.currentTime()))
let floatTimeDu = Float(CMTimeGetSeconds(playerItem.duration))
playProgressView.progress = Double(floatTime / floatTimeDu)
}
}
}

How to detect when AVPlayer video ends playing?

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")
}

Resources