Swift: Apply CIFilter to video error - unfinished AVAsynchronousVideoCompositionRequest deallocated - swift

I'm building a video editor that lets you apply a CIFilter to a video. And it works well.
The only problem I'm facing is that when I dismiss the ViewController I get this error:
Unfinished AVAsynchronousVideoCompositionRequest deallocated - should
have called finishWithComposedVideoFrame:, finishWithError: or
finishCancelledRequest
This error doesn't make the app crash or slower, but when I try to edit another video the preview in the AVPlayer becomes black.
This is my current code:
var mutableComposition = AVMutableVideoComposition()
let exposureFilter = CIFilter.exposureAdjust()
override func viewDidLoad() {
updateComposition()
}
func updateComposition() {
mutableComposition = AVMutableVideoComposition(asset: player.currentItem!.asset, applyingCIFiltersWithHandler: { [weak self] request in
guard let self = self else {
return
}
self.exposureFilter.inputImage = request.sourceImage.clampedToExtent()
self.exposureFilter.ev = 5
let output = self.exposureFilter.outputImage!.cropped(to: request.sourceImage.extent)
request.finish(with: output, context: nil)
})
player.currentItem?.videoComposition = mutableComposition
}
If I remove the [weak self] no error it's printed, but it keeps the ViewController in memory when I dismiss it, creating an unwanted memory leak.

Related

Memory leak when displaying a modal view and dismissing it

When an AVExportSession is finished exporting, I have my app display a modal view displaying the video and an array of images. Dismissing the modal view, and making it display again over and over shows a memory increase that continuously grows. I'm suspicious of a strong reference cycle that could be occurring.
I'm setting required variables on the modal view (manageCaptureVC). fileURL is a global variable that manageCaptureVC can read from to get the video. The video is removed based on that URL when the modal view is dismissed. The leak is larger depending on the size of the media that is captured and displayed in the modal view.
I have used the Leaks Instrument. Unfortunately, it never points to any of my functions. It shows memory addresses that displays assembly language. I am also using a device.
Here is a screen shot of my leaks instrument at the point I display and dismiss my view, and the instrument indicates leaks:
Anything obvious what could cause a leak in my case?
Presenting the modal view (manageCaptureVC)
// video done exporting
guard let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else { return }
exporter.outputURL = mainVideoURL
exporter.outputFileType = AVFileType.mov
let manageCaptureVC = self.storyboard?.instantiateViewController(withIdentifier: "ManageCaptureVC") as! ManageCaptureVC
exporter.exportAsynchronously(completionHandler: {[weak self]
() -> Void in
let fileManagement = FileManagement()
fileManagement.checkForAndDeleteExportFile() // delete export file
self?.myTimer.invalidate()
fileURL = mainVideoURL
guard let imgCaptureModeRawVal = self?.imageCaptureMode.rawValue else { return }
manageCaptureVC.imageCaptureMode = ManageCaptureVC.imageCaptureModes(rawValue: imgCaptureModeRawVal)!
manageCaptureVC.delegate = self
DispatchQueue.main.async(){
manageCaptureVC.modalPresentationStyle = .fullScreen
self?.present(manageCaptureVC, animated: true, completion: nil)
}
})
Dismissing the view:
func goBackTask(){
// turn off manage capture tutorial if needed
if debug_ManageCaptureTutorialModeOn {
debug_ManageCaptureTutorialModeOn = false
delegate?.resetFiltersToPrime()
}
// no longer ignore interface orientation
ignoreSelectedInterfaceOrientation = false
// remove observer for the application becoming active in this view
NotificationCenter.default.removeObserver(self,
name: UIApplication.didBecomeActiveNotification,
object: nil)
if let videoEndedObs = self.videoEndedObserver {
NotificationCenter.default.removeObserver(videoEndedObs)
}
// invalidate thumb timer
thumbColorTimer.invalidate()
// empty UIImages
uiImages.removeAll()
// delete video
let fileManagement = FileManagement()
fileManagement.checkForAndDeleteFile()
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.enableButtons(enabled:false)
if let p = self.player, let pl = self.playerLayer {
p.pause()
pl.removeObserver(self, forKeyPath: "videoRect")
pl.removeFromSuperlayer()
p.replaceCurrentItem(with: nil)
}
group.leave()
}
let group2 = DispatchGroup()
group.notify(queue: .main) {
group2.enter()
DispatchQueue.main.async {
self.enableButtons(enabled:true)
group2.leave()
}
}
group2.notify(queue: .main) {
self.dismiss(animated: true)
}
}
I came across this problem as well. It took me days to track it down.
Setting modalPresentationStyle to .fullScreen resulted in the View Controller not being released. I was able to reproduce this on a trivially simple example.
I got round it by setting modalPresentationStyle to .currentContext.
None of the Instruments identified this retain cycle - I guess because it was in low level Apple code.

Getting Photo Data from AVCapturePhotoCaptureDelegate

I'm currently trying to build a camera application for fun and ran into a problem.
I was using an example that Apple provides you for the process of taking a photo or video. Found here:
https://developer.apple.com/library/content/samplecode/AVCam/Introduction/Intro.html
I'm having a problem using the PhotoCaptureDelegate. So currently when the user takes a photo it will save it to the users photo library. What I would like to do, is after it captures the photo I want to store the photoData Object of the photo. So that I can display the photo on another view as a preview before you save it.
This is what Apple suggests to use:
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishCaptureForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
if let error = error {
print("Error capturing photo: \(error)")
didFinish()
return
}
guard let photoData = photoData else {
print("No photo data resource")
didFinish()
return
}
PHPhotoLibrary.requestAuthorization { [unowned self] status in
if status == .authorized {
PHPhotoLibrary.shared().performChanges({ [unowned self] in
let creationRequest = PHAssetCreationRequest.forAsset()
creationRequest.addResource(with: .photo, data: photoData, options: nil)
if let livePhotoCompanionMovieURL = self.livePhotoCompanionMovieURL {
let livePhotoCompanionMovieFileResourceOptions = PHAssetResourceCreationOptions()
livePhotoCompanionMovieFileResourceOptions.shouldMoveFile = true
creationRequest.addResource(with: .pairedVideo, fileURL: livePhotoCompanionMovieURL, options: livePhotoCompanionMovieFileResourceOptions)
}
}, completionHandler: { [unowned self] success, error in
if let error = error {
print("Error occurered while saving photo to photo library: \(error)")
}
self.didFinish()
}
)
}
else {
self.didFinish()
}
}
}
How would I take the photoData object from my delegate and pass it back to my view controller, that is calling on this delegate?
There are a couple ways to solve this. You could also have your view controller own the photo capture delegate (like Apple's example) and pass it back up after the photo is taken. You could also just have your view controller be the photo capture delegate and not worry about passing anything!
1) Pass the photo back up
There are (again) a couple ways to do this. You could have the delegate pass the value back up to the controller. You could also have the delegate tell the controller it is done, and have the controller come grab the photo. I'll explain the second way below.
Looking at Apple's example, we can see that the PhotoCaptureDelegate already tells the controller when it is done! Whenever a PhotoCaptureDelegate object is created, it is passed a completed block.
To to allow the controller to grab the photo, we just have to make the photo object public!
Change the private var photoData: Data? = nil in PhotoCaptureDelegate to be public var photoData: Data? = nil
In CameraViewController when you define the completed block, you now have the photo data:
...
, completed: { [unowned self] photoCaptureDelegate in
self.sessionQueue.async { [unowned self] in
self.inProgressPhotoCaptureDelegates[photoCaptureDelegate.requestedPhotoSettings.uniqueID] = nil
if let photoData = photoCaptureDelegate.photoData {
// You now have the photo data!
// You can pass this to another method in the Controller now
}
}
}
...
2) View Controller as the AVCapturePhotoCaptureDelegate
Another way to solve this is to just have your ViewController be the AVCapturePhotoCaptureDelegate. Then you'll already have the photo data in the view controller! It could look something like this:
class MyViewController: UIViewController, AVCapturePhotoCaptureDelegate {
...
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishCaptureForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
guard let photoData = photoData else {
return
}
// You now have your photo data!
}
}

Crash when using PHCachingImageManager().requestAVAsset

I am using PHCachingImageManager().requestAVAsset to load some videos from the camera roll:
override func viewDidLoad() {
super.viewDidLoad()
print("SEGUE SUCCESSFUL")
view.backgroundColor = .black
avPlayerLayer = AVPlayerLayer(player: avPlayer)
view.layer.insertSublayer(avPlayerLayer, at: 0)
var asset2:AVAsset? = nil
PHCachingImageManager().requestAVAsset(forVideo: (vidLocation?[videoSender]!)!, options: nil, resultHandler: {(asset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable : Any]?) in
asset2 = asset! as AVAsset
})
let playerItem = AVPlayerItem(asset: asset2!)
avPlayer.replaceCurrentItem(with: playerItem)
}
However when I run the program it pauses at the PHCachingImageManager().requestAVAsset line and shows:
THREAD 1 : EXC_BREAKPOINT
(highlighted in green)
I'm not sure what is happening and can't find anything I understand in the documentation. How do I fix this?
There are a couple of things you're going to want to do here in order to get this working.
You need to cache the PHCachingImageManager as a property on an object that will stay alive. If you just create it without storing it somewhere, ARC rules will cause it to be thrown away. In the code below, I use a lazy var, but that's not the only way to do it.
You should eliminate all forced unwrapped optionals ! in your code. Using the guard let ... or if let ... patterns may feel like more typing but it will save you a lot of time and frustration in the end. Think of the ! as a danger sign that says "CRASH HERE!".
You need to set up the AVPlayerItem from the resultHandler completion block. requestAVAsset is asynchronous so that it doesn't block your main thread while it does the potentially expensive work of retrieving your asset. Basically, as soon as you call requestAVAsset a separate thread goes and does work for you, while the main thread continues working on the rest of the code in the viewDidLoad method. When it has successfully retrieved the AVAsset it calls back to the block of code you provided originally (on the main thread) so you can continue processing.
Here's your code rewritten to incorporate the changes I'm suggesting:
lazy var imageManager = {
return PHCachingImageManager()
}()
override func viewDidLoad() {
super.viewDidLoad()
print("SEGUE SUCCESSFUL")
view.backgroundColor = .black
avPlayerLayer = AVPlayerLayer(player: avPlayer)
view.layer.insertSublayer(avPlayerLayer, at: 0)
var asset2:AVAsset? = nil
guard let phAsset = vidLocation?[videoSender] else { return } //No video
imageManager.requestAVAsset(forVideo: phAsset, options: nil, resultHandler: {(asset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable : Any]?) in
if let avAsset = asset {
self.play(asset: avAsset)
}
})
func play(asset: AVAsset) {
let playerItem = AVPlayerItem(asset: asset)
avPlayer.replaceCurrentItem(with: playerItem)
}
Let me know if anything is unclear.

Low res image taking too long to load

Using Facebook Graph API, I retrieved a string URL to a 200x200 profile picture that I want to display in a UIImageView. I'm successfully able to do this, but I notice that it can take as long as 10 seconds for the image to display on the screen. Can anyone give me some pointers (no pun intended) on how to optimize it?
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: self.profilePictureUrl)!, completionHandler: { (data, response, error) ->
Void in
self.profilePictureImageView.image = UIImage(data: data!)
self.profilePictureImageView.layer.cornerRadius = self.profilePictureImageView.frame.size.width / 2;
self.profilePictureImageView.clipsToBounds = true
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.view.addSubview(self.profilePictureImageView)
})
}).resume()
}
You should move all UIView calls (so anything you set on the UIImageView) onto the main thread as UIKit for the most part isn't thread-safe. You can instantiate the UIImage on the background thread though for performance optimization, so try this:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let url = NSURL(string: self.profilePictureUrl)!
NSURLSession.sharedSession().dataTaskWithURL(
url,
completionHandler: { [weak self] (data, response, error) -> Void in
guard let strongSelf = self else { return }
// create the UIImage on the background thread
let image = UIImage(data: data!)
// then jump to the main thread to modify your UIImageView
dispatch_async(dispatch_get_main_queue(), { [weak self] () -> Void in
guard let strongSelf = self else { return }
let profilePictureImageView = strongSelf.profilePictureImageView
profilePictureImageView.image = image
profilePictureImageView.layer.cornerRadius = profilePictureImageView.frame.size.width / 2;
profilePictureImageView.clipsToBounds = true
strongSelf.view.addSubview(profilePictureImageView)
})
}
).resume()
}
Note also that I've weak-ified your references to self. There is no guarantee the user hasn't dismissed the view controller that is initiating this code by the time the completion routines get called so you want to make sure you're not keeping a strong reference to self. This allows the view controller to deallocate if the user dismisses it and the completion routines then return early without doing any unnecessary work.
This code is illegal:
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: self.profilePictureUrl)!, completionHandler: { (data, response, error) ->
Void in
self.profilePictureImageView.image = UIImage(data: data!)
Stop! you are setting a UIImageView's image on a background thread. No no no. UIKit is not thread-safe. You must get onto the main thread to do this. (You do eventually get onto the main thread in your code, but you are doing it too late.)

UIImageView is NIL

I have a default image in viewItem to make sure that it is working, it shows on the detail view of the splitview.
#IBOutlet weak var ImageView: UIImageView!
var imageCache = [String: UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
self.configureView()
}
func configureView() {
if let detail: AnyObject = self.detailItem {
if let label = self.detailDescriptionLabel {
let dict = detail as [String: String]
label.text = ""
let s = dict["result"]
let vr = NString(string: s!)
let vrd = vr.doubleValue
let value = ceil(vrd*20)
let valueString = String(format: "%.0f", value)
vresult.text = "\(valueString)%"
getPic(dict) // <---- trouble maker
fitem.hidden = false
ritem.hidden = false
}
} else {
navigationController?.popViewControllerAnimated(true)
}
}
func getPic(item: [String: String]) {
var chachedImage = self.imageCache[item["image"]!]
println(item["image"]) // <-- prints out the url
if cachedImage == nil {
var imgUrl = NSURL(string: item["image"]!)
let request: NSURLRequest = NSURLRequest(URL: imgUrl!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {( reponse: NSURLResponse!, data: NSData!, error; NSError!) -> Void in
if error == nil {
cachedImage = UIImage(data: data)
println("got here no problem") // <-- prints out
self.imageCache[item["image"]!] = cachedImage
println(self.imageCache) // <-- prints reference OK
dispatch_async(dispatch_get_main_queue(), {
self.ImageView.image = cachedImage // <---- offender
})
} else {
println("Error: \(error.localizedDescription)")
}
})
} else {
dispatch_async(dispatch_get_main_queue(), {
self.ImageView.image = cachedImage
})
}
}
ImageView is coming up nil every time.
fatal error: unexpectedly found nil while unwrapping an Optional value
but the default image shows. I've moved this out of the dispatch and even tried setting it straight from the viewDidLoad() always errors. It used to be a UIWebView and worked perfectly except that it would not cache anything. Since loading these images is a lot of work, I thought caching would be good, I've got caching working for thumbnails in the MASTER view.
It may be because of how your instaciating your viewcontroller.
let vc = MyViewController()
Something like this wont work. You're creating the VC without actually giving the storyboard a chance to link the IBOutlets. Instead use
storyboard.instantiateViewControllerWithIdentifier(identifier: String)
You may need to get reference to the storyboard using
let storyboard = UIStoryboard(name: name, bundle: NSBundle.mainBundle())
Hope this helps :)
Changing your variable name shouldn't make any difference except for readibility/maintainability unless there's a namespace conflict (good to understand why/where that might be happening). Also I was wondering - you made the IBOutlet'ed varable weak. When the last remaining strong ref to the object goes away, the weak references to the object are set nil by the runtime/garbage collector automatically. (Look up that section of the Swift documentation if you're not solid about it).
Maybe you should check your classes and controllers by adding deinit { println(,"function name deallocated' }. Between your use of weak and improved behavior seen when you change the variable name, it seems like there might be some weird (buggy) interactions going on in your app itself.
Well silly me. I've been working on this for a few days, I got the great idea to try and change the name, and it worked. I tried changing it back and it broke, apparently you can't use ImageView as a variable!
In my case was because I was using a nib and didn't register it.
Once I did registered it, it worked
My case Was Different I used
awakeFromNib()
instead of
viewDidLoad()
.