9

I want to grab frames from a video at a specific time. I'm calling my grab-frame-function with a time specified as seconds as a Float64. The problem is that it doesn't grab the current frame. It seems to ignore the decimals. If I call the function with for example 1.22 and 1.70 it will return the same frame. I'm quite new when it comes to Swift so I guess I don't get the CMTime object right. So can anyone see what's wrong with this?

func generateThumnail(url : NSURL, fromTime:Float64) -> UIImage {
    var asset :AVAsset = AVAsset.assetWithURL(url) as! AVAsset
    var assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset)
    assetImgGenerate.appliesPreferredTrackTransform = true
    var error       : NSError? = nil
    var time        : CMTime = CMTimeMakeWithSeconds(fromTime, 600)        
    var img         : CGImageRef = assetImgGenerate.copyCGImageAtTime(time, actualTime: nil, error: &error)
    var frameImg    : UIImage = UIImage(CGImage: img)!
    return frameImg
}

var grabTime = 1.22
img = generateThumnail(urlVideo, fromTime: Float64(grabTime)) 
arpo
  • 1,831
  • 2
  • 27
  • 48
  • Your code works ok for me in an iOS Playground: [screenshot 1](https://www.evernote.com/shard/s89/sh/26b0df60-07ab-497a-aad0-fa1d846fb200/f1147c3886afd1f9/res/63c58859-ddec-4b65-98b0-5a577e10ec21/skitch.png), [screenshot 2](https://www.evernote.com/shard/s89/sh/524547e8-9884-42ac-ad17-baa8d7eeb9b4/de65115d4542ffe2/res/071ab169-3913-4d33-8014-110de8a70416/skitch.png) so your problem does not come from misunderstanding CMTime. – Eric Aya Aug 29 '15 at 15:21
  • Thanks for trying. I don't know what I'm doing wrong. I want to grab all frames on a 4 second long clip and store them in an array. But most frames look the same. :/ Maybe it has something to do with the length of the video. – arpo Aug 29 '15 at 15:26
  • Have you tried using a different timescale? 6000 is a lot. From the doc: "if the timescale is 4, each unit represents a quarter of a second; if the timescale is 10, each unit represents a tenth of a second, and so on." With your timescale I also had some sampled images being the same from my short clip, but not anymore with much smaller timescales. – Eric Aya Aug 29 '15 at 15:44
  • I read somewhere that 600 was good for video. I'll try experimenting with that. Maybe the FPS is a good value? – arpo Aug 29 '15 at 15:47
  • I think you should adapt the timescale to the length of your video. If the units are too small, several units (or even a range of units) could indeed correspond to the same frame. I have no magic number to suggest, though. – Eric Aya Aug 29 '15 at 15:52
  • Excellent! :) thanks – arpo Aug 29 '15 at 15:55
  • 1
    Here's a tip: instead of passing nil to actualTime, prepare `var actual : CMTime = CMTimeMake(0, 0)` and pass it like this: `var img = assetImgGenerate.copyCGImageAtTime(time, actualTime: &actual, error: &error)` then inspect the contents with: `let inf = CMTimeCopyAsDictionary(actual, kCFAllocatorDefault)` `println(inf)`. This dictionary contains the actual properties used for the grab, it's useful for debugging and finding a good timescale. – Eric Aya Aug 29 '15 at 16:07
  • 1
    There's an interesting post here: http://stackoverflow.com/questions/29425455/ios-take-multiple-screen-shots with more precise information. – Eric Aya Aug 29 '15 at 16:29

3 Answers3

15

Thanks to @eric-d who found this post: iOS Take Multiple Screen Shots

I manage to find out that adding:

    assetImgGenerate.requestedTimeToleranceAfter = kCMTimeZero;
    assetImgGenerate.requestedTimeToleranceBefore = kCMTimeZero;

...to my function will do the trick.

My updated function looks like this:

func generateThumnail(url : NSURL, fromTime:Float64) -> UIImage {
    var asset :AVAsset = AVAsset.assetWithURL(url) as! AVAsset
    var assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset)
    assetImgGenerate.appliesPreferredTrackTransform = true
    assetImgGenerate.requestedTimeToleranceAfter = kCMTimeZero;
    assetImgGenerate.requestedTimeToleranceBefore = kCMTimeZero;
    var error       : NSError? = nil
    var time        : CMTime = CMTimeMakeWithSeconds(fromTime, 600)        
    var img         : CGImageRef = assetImgGenerate.copyCGImageAtTime(time, actualTime: nil, error: &error)
    var frameImg    : UIImage = UIImage(CGImage: img)!
    return frameImg
}

var grabTime = 1.22
img = generateThumnail(urlVideo, fromTime: Float64(grabTime))
Community
  • 1
  • 1
arpo
  • 1,831
  • 2
  • 27
  • 48
  • 2
    update required for later swift versions: do { let img = try assetImgGenerate.copyCGImageAtTime(time, actualTime: nil) return UIImage(CGImage: img) } catch let error as NSError { print("Image generation failed with error \(error)") return nil } – Pichirichi Jun 09 '16 at 15:16
  • well.. spent 2 hours to find out.. this should def. be default or is there any reason for not being default? – Lukas Sep 10 '18 at 17:30
4

For swift 4.2

fileprivate func generateThumnail(url : URL, fromTime:Float64) -> UIImage? {
    let asset :AVAsset = AVAsset(url: url)
    let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset)
    assetImgGenerate.appliesPreferredTrackTransform = true
    assetImgGenerate.requestedTimeToleranceAfter = CMTime.zero;
    assetImgGenerate.requestedTimeToleranceBefore = CMTime.zero;
    let time : CMTime = CMTimeMakeWithSeconds(fromTime, preferredTimescale: 600)
    if let img = try? assetImgGenerate.copyCGImage(at:time, actualTime: nil) {
        return UIImage(cgImage: img)
    } else {
        return nil
    }
}
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
2

I incorporated arpo's answer into my project, updated for Swift 3:

fileprivate func generateThumnail(url : URL, fromTime:Float64) -> UIImage? {
    let asset :AVAsset = AVAsset(url: url)
    let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset)
    assetImgGenerate.appliesPreferredTrackTransform = true
    assetImgGenerate.requestedTimeToleranceAfter = kCMTimeZero;
    assetImgGenerate.requestedTimeToleranceBefore = kCMTimeZero;
    let time        : CMTime = CMTimeMakeWithSeconds(fromTime, 600)
    if let img = try? assetImgGenerate.copyCGImage(at:time, actualTime: nil) {
        return UIImage(cgImage: img!)
    } else {
        return nil
    }
}
ReactiveRaven
  • 7,203
  • 2
  • 29
  • 38
Andrew Aarestad
  • 1,120
  • 2
  • 11
  • 15