swiftavfoundationavassetavassetimagegenerator

Retrieving UIImage from video only when function completed creating them - swift - swift programmatically


I'm creating UIImages from video using generateCGImagesAsynchronouslyForTimes :

fileprivate func createImageFramesFromVideo(completion: @escaping ([UIImage]) -> Void) {
    guard let url = self.videoUrl else {
        return
    }
    
    let asset = AVAsset(url: url)
    let imageGenerator = AVAssetImageGenerator(asset: asset)
    let videoDuration = asset.duration
    
    //getting image snapshots more or less in half the video
    let time1 = CMTime(value: videoDuration.value/2 - videoDuration.value/10, timescale: videoDuration.timescale)
    let time2 = CMTime(value: videoDuration.value/2 - videoDuration.value/11, timescale: videoDuration.timescale)
    let time3 = CMTime(value: videoDuration.value/2 - videoDuration.value/12, timescale: videoDuration.timescale)
    let time4 = CMTime(value: videoDuration.value/2, timescale: videoDuration.timescale)
    let time5 = CMTime(value: videoDuration.value/2 + videoDuration.value/12, timescale: videoDuration.timescale)
    let time6 = CMTime(value: videoDuration.value/2 + videoDuration.value/11, timescale: videoDuration.timescale)
    let time7 = CMTime(value: videoDuration.value/2 + videoDuration.value/10, timescale: videoDuration.timescale)
    
    var imageArray : [UIImage] = []
    
    imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time1), NSValue(time: time2), NSValue(time: time3), NSValue(time: time4), NSValue(time: time5), NSValue(time: time6), NSValue(time: time7)]) { (time, image, time1, result, err) in
        if let err = err {
            print("there's an error in retrieving the images", err)
        }
       
        let theImage = UIImage(cgImage: image!)
        
        imageArray.append(theImage)
        
        if(result == .succeeded){
            completion(imageArray)
        }
    }
}

Since the completion handler of the function is called each time a new image is created, completion() is called multiple times.

I want to achieve that completion() is called only after all the images (in this case 7) are created. How can I do it?


Solution

  • You need to use a DispatchGroup to wait for several async functions to complete. You need to make sure your calls to DispatchGroup.enter() and leave() are balanced (called the same number of times), since when you use dispatchGroup.notify(queue:), the closure will only be executed once leave() has been called for each enter().

    You need to call enter for each element of your times array (which will contain the CMTime variables time1 to time7, then in the completion block of generateCGImagesAsynchronously(forTimes:, you call leave(). This ensures that the notify(queue:) will only execute its closure once generateCGImagesAsynchronously called its own completion for all images.

    fileprivate func createImageFramesFromVideo(completion: @escaping ([UIImage]) -> Void) {
        guard let url = self.videoUrl else { return }
    
        let asset = AVAsset(url: url)
        let imageGenerator = AVAssetImageGenerator(asset: asset)
        let videoDuration = asset.duration
    
        //getting image snapshots more or less in half the video
        let time1 = CMTime(value: videoDuration.value/2 - videoDuration.value/10, timescale: videoDuration.timescale)
        let time2 = CMTime(value: videoDuration.value/2 - videoDuration.value/11, timescale: videoDuration.timescale)
        let time3 = CMTime(value: videoDuration.value/2 - videoDuration.value/12, timescale: videoDuration.timescale)
        let time4 = CMTime(value: videoDuration.value/2, timescale: videoDuration.timescale)
        let time5 = CMTime(value: videoDuration.value/2 + videoDuration.value/12, timescale: videoDuration.timescale)
        let time6 = CMTime(value: videoDuration.value/2 + videoDuration.value/11, timescale: videoDuration.timescale)
        let time7 = CMTime(value: videoDuration.value/2 + videoDuration.value/10, timescale: videoDuration.timescale)
        let times = [NSValue(time: time1), NSValue(time: time2), NSValue(time: time3), NSValue(time: time4), NSValue(time: time5), NSValue(time: time6), NSValue(time: time7)]
    
        var imageArray : [UIImage] = []
    
        let dispatchGroup = DispatchGroup()
        times.forEach { _ in
            dispatchGroup.enter()
        }
    
    
        imageGenerator.generateCGImagesAsynchronously(forTimes: times) { (time, image, time1, result, err) in
            if let err = err {
                print("there's an error in retrieving the images", err)
            }
    
            let theImage = UIImage(cgImage: image!)
    
            imageArray.append(theImage)
            dispatchGroup.leave()
        }
    
        dispatchGroup.notify(queue: .main) {
            completion(imageArray)
        }
    }