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