AVPlayer Crashes on Device When Dismissing View - ios

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

Related

Too many AVPlayers causes Terminated due to memory issue

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

Dismissing AVPlayerViewController doesn't 'kill' the object - it's persisting

I am playing videos that are in my app bundle.
They are playing correctly.
However, when I call to dismiss the AVPlayerViewController, it visibly is removed from the view hierarchy but, if I turn off the iOS device and turn it back on again, on the lock screen there is a media control showing that video and a 'play' button.
If you touch play you only get the audio and no video.
My problem is I don't understand why the 'dismiss' is not completely 'killing' the player when I'm done with it.
Here is the presentation code:
let path = Bundle.main.path(forResource: filename, ofType: type)
let url = NSURL(fileURLWithPath: path!)
let player = AVPlayer(url: url as URL)
NotificationCenter.default.addObserver(self,
selector: #selector(VideoLibraryViewController.didFinishPlaying(notification:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: player.currentItem)
self.playerController = AVPlayerViewController()
self.playerController?.player = player
self.playerController?.allowsPictureInPicturePlayback = true
self.playerController?.showsPlaybackControls = YES
self.playerController?.delegate = self
self.playerController?.player?.play()
self.present(self.playerController!, animated: true, completion : nil)
Here is the dismissal code:
// Delegate can implement this method to be notified when Picture in Picture will start.
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
self.playerController?.dismiss(animated: NO, completion: nil )
}
And here's what's remaining in the systemwide media player that is shown on the lock screen / control centre:
iOS 13 SDK ONLY: Here's the solution, but the answer is that despite dismissing the AVPlayerViewController, the AVPlayer object that it's knows about is persistent and that needs to be set to nil.
private func killVideoPlayer()
{
self.playerController?.player?.pause()
self.playerController?.player = nil
self.playerController?.dismiss(animated: YES, completion: { self.playerController = nil })
}
Previous SDK's, this still isn't working.
Neither is setting the AVAudioSession.active to false... ?!?! Still need a pre iOS 13 SDK solution.

Strange behavior of AVPlayer

I noticed a very odd behavior when working with AVPlayer from AVFoundation framework. I'm trying to stream (play) some mp3 radio file from internet.
So I created a new project in Xcode and added the following code to the default ViewController:
import UIKit
import AVFoundation
class ViewController: UIViewController {
let player: AVPlayer = AVPlayer()
override func viewDidLoad() {
super.viewDidLoad()
let asset = AVAsset(url: URL(string: "https://rfcmedia.streamguys1.com/Newport.mp3")!);
let playerItem = AVPlayerItem(asset: asset);
player.replaceCurrentItem(with: playerItem)
player.play();
}
}
There's nothing particularly interesting going on there. I just create an AVPlayerItem instance with the radio station URL and give it to AVPlayer and then ask the player object to play the song and it works just as expected.
However if I remove the player object instantiation from the class block and put it in the viewDidLoad method the code simply doesn't work (doesn't play anything) yet it doesn't crash or spit out any kinds of errors or warning.
import UIKit
import AVFoundation
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let player: AVPlayer = AVPlayer()
let asset = AVAsset(url: URL(string: "https://rfcmedia.streamguys1.com/Newport.mp3")!);
let playerItem = AVPlayerItem(asset: asset);
player.replaceCurrentItem(with: playerItem)
player.play();
}
}
I can't understand this behavior. Why is that happening? Is it limited to AVPlayer or I should watch out for cases like this?
As Andrea Mugnaini explained in the comments to my post, my second example is going to cause an immediate deallocation by the ARC (Automatic Reference Counting) as we don't keep a strong reference to the AVPlayer object.
This is a common pitfall of developers new to Apple's ecosystem.

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.

How to stream audio for only a known duration using swift

I'm using AVPlayer (I don't need to, but I wanna stream it and start playing as soon as possible) to play an m4a file (it's an iTunes audio preview). Only I only want it to play a part of that file.
I'm able to set a start time but not an end time.
Using a timer is not working because I'm using URL as a http address. I'm playing as it loads, without downloading the file.
I also saw solutions in Objective-C to use KVO to know when music starts playing but I'm thinking this is not the best approach since I'm using swift and also because of glitches that may occur so the song will not stop at the right moment.
You can add a addBoundaryTimeObserverForTimes to your AVPlayer as follow:
update: Xcode 8.3.2 • Swift 3.1
import UIKit
import AVFoundation
class ViewController: UIViewController {
var player: AVPlayer!
var observer: Any!
override func viewDidLoad() {
super.viewDidLoad()
guard let url = URL(string: "https://www.example.com/audio.mp3") else { return }
player = AVPlayer(url: url)
let boundary: TimeInterval = 30
let times = [NSValue(time: CMTimeMake(Int64(boundary), 1))]
observer = player.addBoundaryTimeObserver(forTimes: times, queue: nil) {
[weak self] time in
print("30s reached")
if let observer = self?.observer {
self?.player.removeTimeObserver(observer)
}
self?.player.pause()
}
player.play()
print("started loading")
}
}

Resources