Too many AVPlayers causes Terminated due to memory issue - ios

I have a vc that has an AVPlayer inside of it. From that vc I can push on a different vc with another player inside of it and I can keep pushing on more vcs with a player inside them also. After about the 14th vc getting pushed on the app crashes with Terminated due to memory issue.
When I look at the memory graph (9th icon in left pane) it's at about 70mb so there isn't an obscene jump in memory. All of my video files are saved to and retrieved from disk and whenever I pop a vc I have a print statement inside Deinit that always runs so there isn't anything else causing the memory issue. This led me to believe the other SO answers that said that there is a limit to 16 AVPlayers at the same time. The reason I think all of these players are causing this memory crash is because once I comment out the player initialization code I can push on 30 vcs with no crashes whatsoever.
I was about to completely remove the player, playerItem, its observers, and player layer from the parent vc in viewWillDisappear/viewDidDisappear and then once the child is popped reinitialize everything again in viewWillAppear/viewDidAppear but then I came across this blog that says
platform limitation on the number of video “render pipelines” shared
between apps on the device. It turns out that setting the AVPlayer to
nil does not free up a playback pipeline and that it is actually the
association of a playerItem with a player that creates the pipeline in
the first place
and this answer that says
It is not a limit on the number of instances of AVPlayer, or
AVPlayerItem. Rather,it is the association of AVPlayerItem with an
AVPlayer which creates a "render pipeline"
The question is when pushing/popping on a new vc (it will have a player inside of it) do I need to completely remove/readd everything associated with the player or will setting the AVPlayerItem to nil then reinitializing it again resolve the issue?
If the render pipelines are causing the problem it would seem that the limit isn't on the players but on the playerItems.
code:
override func viewDidLoad() {
super.viewDidLoad()
configurePlayer(with: self.videoUrl)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// only runs when popping back
if !isMovingToParent {
// I can either
let asset = AVAsset(url: selfvideoUrl)
self.playerItem = AVPlayerItem(asset: asset)
self.player?.replaceCurrentItem(with: playerItem!)
// or just reinitialize everything
configurePlayer(with: self.videoUrl)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// would these 2 lines be enough suffice to prevent the issue?
self.player?.replaceCurrentItem(with: nil)
self.playerItem = nil
// or do I also need to nil out everything?
self.player = nil
self.avPlayerView.removeFromSuperView()
self.playerStatusObserver = nil
self.playerRateObserver = nil
self.playerTimeControlStatusObserver = nil
}
func configurePlayer(with videoUrl: URL) {
let asset = AVAsset(url: videoUrl)
self.playerItem = AVPlayerItem(asset: asset)
self.player = AVPlayer()
self.playerLayer = AVPlayerLayer(player: player)
self.playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
self.player?.automaticallyWaitsToMinimizeStalling = false
self.playerItem.preferredForwardBufferDuration = TimeInterval(1.0)
view.addSubview(avPlayerView) // this is just a view with a CALayer for the playerLayer
self.playerLayer?.frame = avPlayerView.bounds
self.avPlayerView.layer.addSublayer(playerLayer!)
self.avPlayerView.playerLayer = playerLayer
self.player?.replaceCurrentItem(with: playerItem!)
// add endTimeNotification
setNSKeyValueObservers()
}
func setNSKeyValueObservers() {
self.playerStatusObserver = player?.observe(\.currentItem?.status, options: [.new, .old]) {
[weak self] (player, change) in ... }
self.playerRateObserver = player?.observe(\.rate, options: [.new, .old], changeHandler: {
[weak self](player, change) in ... }
self.playerTimeControlStatusObserver = player?.observe(\.timeControlStatus, options: [.new, .old]) {
[weak self](player, change) in ... }
}

I just tested it and setting this to nil and reinitializing it is what allowed me to push on 30 vcs each with an AVPlayer inside of each one without any crashes whatsoever.
player?.replaceCurrentItem(with: nil)
So the issue isn't the total amount of AVPlayers, it's like this guy said, the association of AVPlayerItem with an AVPlayer which creates a "render pipeline and too many of them at the same time is what causes the problem.
override func viewDidLoad() {
super.viewDidLoad()
configurePlayer(with: self.videoUrl)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let playerItem = playerItem {
self.player?.replaceCurrentItem(with: playerItem)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.player?.replaceCurrentItem(with: nil)
}

Related

AVPlayerViewController memory leak/retain cycle?

I keep getting a problem with my app where AVPlayerViewController won't get purged from memory after being dismissed, it just builds up more after each presentation.
This is on iOS 12, try the following in an empty project, I do it in viewDidAppear for instance to automatically make it pop up a few seconds after it's dismissed. You'll notice after a bunch of dismissals if you go to the "Debug Memory Graph" tool at the bottom of Xcode that AVPlayerViewController stays in memory and there's a bunch of instances of it.
The key though is LET THE VIDEO PLAY UNTIL THE END where the controls pop back up.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(6)) { [weak self] in
let assetURL = URL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")!
let player = AVPlayer(url: assetURL)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self?.navigationController?.present(playerViewController, animated: true) {
playerViewController.player!.play()
}
}
}
Try this
self?.navigationController?.present(playerViewController, animated: true) { [weak playerViewController]
playerViewController?.player!.play()
}

IOS Custom UIViewController Error

In my login screen of my Ios app I am trying to display a video. I saw on stack overflow a great example on how to do such thing. I tried the code out for myself and ran into an error. I was going to comment my error on the original post but my account did not have enough reputation to do so. My code when I run on my simulated device returns to me an error that says:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
I managed to track down there error using breakpoints to line 12 which is avPlayer = AVPlayer(url: video!)
Though here is my full code:
import AVFoundation
import UIKit
class VideoBackgroundController: UIViewController {
var avPlayer: AVPlayer!
var avPlayerLayer: AVPlayerLayer!
var paused: Bool = false
override func viewDidLoad() {
let video = Bundle.main.url(forResource:"space", withExtension: "mp4")
//This is where my the error happens
avPlayer = AVPlayer(url: video!)
avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
avPlayer.volume = 0
avPlayer.actionAtItemEnd = .none
avPlayerLayer.frame = view.layer.bounds
view.backgroundColor = .clear
view.layer.insertSublayer(avPlayerLayer, at: 0)
NotificationCenter.default.addObserver(self,
selector: #selector(playerItemDidReachEnd(notification:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: avPlayer.currentItem)
}
func playerItemDidReachEnd(notification: Notification) {
let p: AVPlayerItem = notification.object as! AVPlayerItem
p.seek(to: kCMTimeZero)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
avPlayer.play()
paused = false
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
avPlayer.pause()
paused = true
}
}
And I do have a mp4 video named "space" in my project files.
Any explanation to how or why this is happening and what actions I can take to fix this issue would be greatly appreciated.
I have checked and your code is running fine.
Just check that, the video should be under in your "Copy bundle Resources" of "Build Phases" tab in Target .
If it is not there , Please add the same as in below screenshots.
You are assigning non optional value of video. Make it optional, ? Swift means – It can have a value, but it can also be nil. It is defined by “?”.Please look below code
avPlayer = AVPlayer(url: video?)
if avPlayer != nil
{ // Video url is not empty
}
else
{ // It is empty
}

tvOS app memory issue: How to resolve it?

I have a tvOS app which has a video playing in it.
There are basically two videos (different speed versions of the same video). One is 12MB in size and another is 1.9MB.
When the app starts, it runs fine (Xcode shows 191MB). However, when clicking normal speed button once, the memory shoots to 350MB. As and when I click normal and fast buttons respectively, this goes on increasing and at one point it becomes 1GB+. You can see the attachment. It even went to 3GB when the video stuttered and the app stopped.
Is there any way to solve the memory issue and save the app from stopping?
Another problem is: when in Apple TV, we go to another app from this app and come back, the video again stops. However, in Simulator, it is not happening. Can someone help me to solve these two issues?
Here is the code I am using:
var avPlayerLayer: AVPlayerLayer!
var paused: Bool = false
func playmyVideo(myString: String) {
let bundle: Bundle = Bundle.main
let videoPlayer: String = bundle.path(forResource: myString, ofType: "mov")!
let movieUrl : NSURL = NSURL.fileURL(withPath: videoPlayer) as NSURL
print(movieUrl)
viewVideo.playVideoWithURL(url: movieUrl)
}
#IBAction func normalPressed(_ sender: Any) {
playmyVideo(myString: "normal")
}
#IBAction func forwardPressed(_ sender: Any) {
playmyVideo(myString: "fast")
}
class VideoPlay: UIView {
private var player : AVPlayer!
private var playerLayer : AVPlayerLayer!
init() {
super.init(frame: CGRect.zero)
self.initializePlayerLayer()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.initializePlayerLayer()
self.autoresizesSubviews = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initializePlayerLayer()
}
private func initializePlayerLayer() {
playerLayer = AVPlayerLayer()
playerLayer.backgroundColor = UIColor.clear.cgColor
playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
self.layer.addSublayer(playerLayer)
playerLayer.frame = UIScreen.main.bounds
}
func playVideoWithURL(url: NSURL) {
player = AVPlayer(url: url as URL)
player.isMuted = false
playerLayer.player = player
player.play()
loopVideo(videoPlayer: player)
}
func toggleMute() {
player.isMuted = !player.isMuted
}
func isMuted() -> Bool
{
return player.isMuted
}
func loopVideo(videoPlayer: AVPlayer) {
NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil, queue: nil) { notification in
let t1 = CMTimeMake(5, 100);
self.player.seek(to: t1)
videoPlayer.seek(to: kCMTimeZero)
self.player.play()
}
}
}
I see two problems in your code:
Each time playVideoWithURL method is called, you create new AVPlayer instance, instead of reusing already existing one. You can call replaceCurrentItem(with:) method on your player property when you want to play another URL.
That itself is a bit inefficient, but shouldn't cause the memory issue you described. I think the reason is:
Each time loopVideo method is called, you pass a closure to NotificationCenter.default.addObserver. This closure creates a strong reference to videoPlayer. You never remove the observer from the notification center.
As loopVideo is called each time you create new AVPlayer instance, these instances are never deallocated, leading to the memory issue you described.
To fix it, you can:
initialize player property only once in playVideoWithURL, then use replaceCurrentItem when you want to play another video
also change the "loop" logic, so that you call NotificationCenter.default.addObserver only once
the closure you pass to NotificationCenter.default.addObserver creates a memory leak (see this question). You can get rid of it by capturing self weakly:
NotificationCenter.default.addObserver(forName:
NSNotification.Name.AVPlayerItemDidPlayToEndTime,object: nil, queue: nil) { [weak self], notification in
self?.player.seek(to: kCMTimeZero)
self?.player.play()
}
also remember to call removeObserver in deinit method of VideoPlay class.

AVPlayer stuck video, but audio works

I go to create AVPlayerItem through AVURLAsset, my code:
let asset = AVURLAsset(URL: safeURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
asset.loadValuesAsynchronouslyForKeys([assetKeyPlayable, self.assetKeyTracks, self.assetKeyHasProtectedContent]) {
() -> Void in
dispatch_async(dispatch_get_main_queue(), {
() -> Void in
// Use the AVAsset playable property to detect whether the asset can be played
if !asset.playable {
let localizedDescription = "Item cannot be played,Item cannot be played description"
let localizedFailureReason = "The assets tracks were loaded, but could not be made playable,Item cannot be played failure reason"
let userInfo = [NSLocalizedDescriptionKey: localizedDescription, NSLocalizedFailureReasonErrorKey: localizedFailureReason]
let error = NSError(domain: "domain", code: 0, userInfo: userInfo)
self.videoPlayerDelegate?.videoPlayer?(self, playerItemStatusDidFail: error)
self.cleanPlayer()
return
}
// At this point we're ready to set up for playback of the asset. Stop observing
if let _ = self.player?.currentItem {
self.cleanPlayer()
}
if asset.URL.absoluteString != safeURL.absoluteString {
return
}
var error: NSError?
let status = asset.statusOfValueForKey(self.assetKeyTracks, error: &error)
var playerItem = AVPlayerItem(URL: safeURL)
if status == .Loaded {
playerItem = AVPlayerItem(asset: asset)
} else {
// You should deal with the error appropriately.If Loaded fails, create an AVPlayerItem directly from the URL
playerItem = AVPlayerItem(URL: safeURL)
}
self.player = self.playerWithPlayerItem(playerItem)
self.registerMonitoring()
self.registerNotification()
self.addTimeObserver()
completionBlock?(loadURLString: playerURL.absoluteString)
})
}
Add AVPlayerLayer display video in my View, my code:
// MARK: - Property
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
When displaying video after completion of loading
self.videoPlayer?.loadPlayer({
[weak self](loadURLString) in
if let strongSelf = self {
strongSelf.player = strongSelf.videoPlayer?.player
strongSelf.startPlay()
}
})
Call seekToTime method to specify the play:
self.player?.currentItem?.seekToTime(CMTimeMakeWithSeconds(time, Int32(NSEC_PER_SEC)), toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) {
[weak self] finished in
if let weakSelf = self {
if weakSelf.isPlaying {
weakSelf.videoPlayerDelegate?.videoPlayerDidplayerItemSeekToTime?(weakSelf)
}
}
}
Some pictures of the stuck interface:
In the first picture the sound is audible, but the interface is stuck.
In the second picture, the video works, but I get no sound.
My question:
When I call the seekToTime method upon completion, sometimes the video has sound, but the interface is stuck, occasionally the video works. I tried to call the CALayer setNeedsDisplay methods, to update the AVPlayerLayer picture, but that didn't help. I don't know what to do anymore, i would be grateful for every and any help.
Since this is happening for many of us in a different way and is not answered, I am answering it here, For me this was happening when I was trying to play the video in UIView using AVPlayer and AVPlayerLayer but when I used the same AVPlayer to load the video in AVPlayerViewController the video was getting stuck and audio kept on playing.
The trick is to destroy the AVPlayerLayer before using the same AVPlayer somewhere else ,Cant believe I found this enlightenment over here.
https://twitter.com/skumancer/status/294605708073263104
How I implemented this?
I created 2 view controllers in my main story board.
1) View Controller Scene
i) This has a UIView in it referenced to IBOutlet 'videoView'
ii) A button with segue navigation to 'show' AV Player View Controller Scene referenced to IBOutlet 'fullscreen'
2) AV Player View Controller Scene
// Code in your controller class.
// Make sure to reference your IBOutlet in your StoryBoard.
#IBOutlet weak var videoView : UIView!
#IBOutlet weak var fullscreen : UIButton!
var player:AVPlayer? = nil;
var playerLayer:AVPlayerLayer? = nil;
override func viewDidLoad() {
super.viewDidLoad()
// Your Url to play, could be hls type video
let url = URL(string: "https://your_url.m3u8");
let asset = AVAsset(url : url!)
let playerItem = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: playerItem)
}
override func viewDidAppear(_ animated: Bool) {
playInUIView()
pauseVideo()
}
// Creates An AVPlayerLayer and adds it as a sublayer in UIView.
func playInUIView(){
// Creating player layer to render video.
playerLayer = AVPlayerLayer(player: player)
playerLayer?.frame = self.videoView.bounds
playerLayer?.videoGravity = .resizeAspect
// Adding a sub layer to display it in UIView
self.videoView.layer.addSublayer(playerLayer!)
}
// Destroyes an AVPlayerLayer and Sublayer of your UIView
func destroyPlayInView(){
self.videoView.layer.sublayers=nil
playerLayer = nil
}
// On button click open AVPlayerViewController.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destination = segue.destination as! AVPlayerViewController
// Destroy AVPlayerLayer before rendering video in AVPlayerViewController.
destroyPlayInView()
// Set same AVPlayer in AVPlayerViewController.
destination.player = self.player
self.present(destination, animated: true) {
destination.player!.play()
}
}
PS - I have not implemented any controls as of yet and there could be coding malpractices as I have just started with swift and IOS development couple of days back, so please let me know wherever I am wrong.
Try this approach as I have encountered the same issue and solve it by this kind approach.
player.pause()
player.currentItem?.seek(to: CMTime(), completionHandler: { (_) in
player.play()
})
The same code of user nferocious76 but in Objective C
[self.player pause];
AVPlayerItem *playerItem = [self.player currentItem];
__weak typeof(self) weakSelf = self;
[playerItem seekToTime:playerItem.currentTime completionHandler:^(BOOL finished){
if (finished){
[weakSelf.player play];
}
}];

AVPlayer Crashes on Device When Dismissing View

I'm having an issue with the AVPlayer. I've tried many solutions but it still crashes after I move to a different view.
class GuideVideo : BaseViewController{
var avPlayer: AVPlayer?
var avPlayerLayer: AVPlayerLayer?
override func viewDidLoad() {
super.viewDidLoad()
generateVideo()
}
func generateVideo () {
let videoURLWithPath = data["VideoUrl"]
let videoFilePath = NSURL(string: videoURLWithPath!)
let avAsset: AVAsset = AVAsset.assetWithURL(videoFilePath) as! AVAsset
let avPlayerItem = AVPlayerItem(asset: avAsset)
avPlayer = AVPlayer(playerItem: avPlayerItem)
avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer!.frame = self.videoView.bounds
self.videoView.layer.addSublayer(avPlayerLayer)
avPlayer!.play()
}
I've also tried removing the observers from it since I assume the crash is related to a nil observer.
override func viewWillDisappear(animated: Bool) {
dispatch_async(dispatch_get_main_queue(),{
if self.avPlayerLayer != nil {
self.avPlayerLayer!.player.pause()
NSNotificationCenter.defaultCenter().removeObserver(self.avPlayerLayer!)
self.avPlayerLayer!.removeFromSuperlayer()
self.avPlayerLayer = nil
}
self.avPlayer!.pause()
self.avPlayer = AVPlayer()
})
super.viewWillDisappear(animated)
}
Nothing works and the crash provides no data. Either it crashes without indicating a line or a general
Thread 1: EXC_BAD_ACCESS
* Import thing to note is that this crash only happens on the iPhone 6/6+. Our iPhone 5C handles the class well.
* I only get the crash after moving to another view controller or a different navigation stack, but a few seconds after the view had been dismissed.
Thank you, been sitting on this for the better part of 2 days now.
EDIT: The issue is apparently related to the SWReveal. It deallocates instances before their lifecycle is over.
Accepted the best solution, but the problem is related to SWReveal.
Try using the MPMoviePlayerController instead of the AVPlayer, for simple solutions, the MPMoviePlayerController has an easier API.
make the MPMoviePlayerController a global instance making it accessible through the whole app, and then initialize it when you need it.
Try playing the videos locally first and check if it solves the problem

Resources