I'm making video from taking snapshots (30 times per sec), and I really need to find the best approach to get snapshot in the background thread with the best possible performance.
I have two approach in UIView extension
extension UIView {
func snapshotInMain(view: UIView) -> UIImage {
let render = UIGraphicsImageRenderer(size: view.bounds.size)
let image = render.image { (context) in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
return image
}
func snapShotInBackground(viewLayer: CALayer, viewBounds: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: viewBounds)
return renderer.image { rendererContext in
viewLayer.render(in: rendererContext.cgContext)
}
}
}
The first one snapshotInMain
completely execute in main thread, it froze the app but the screenshot itself is taken faster and I have a smoother video from it
the second one snapShotInBackground
, the layer and bounds are calculated in the main thread, but then will execute in background, but it's way slower from the the one that execute in main (first one).
it works like that
DispatchQueue.main.async {
let layer = self.selectedView.layer
let bounds = self.selectedView.bounds
DispatchQueue.global(qos: .background).async {
let image = self.selectedView.snapShotInBackground(viewLayer: layer, viewBounds: bounds)
}
}
My job is really depand on it, I'm pretty stuck here and really need help. please help me to find the best option possible. My main requests are 1- App should not freeze 2- The taking snapshot should be fast that I can do it 30 times per sec.
Thank you! Looking forward to your help
CodePudding user response:
You can use a Timer to throttle the snapshots on the main thread. Here's an example
@objc
private func beginTapped() {
print("Start time \(Date.now)")
frameCount = 0
let timer = Timer.scheduledTimer(timeInterval: 0.03, target: self, selector: #selector(takeSnapshot), userInfo: nil, repeats: true)
DispatchQueue.main.asyncAfter(deadline: .now() 5, execute: {
timer.invalidate()
print("Finished at \(Date.now) with \(self.frameCount) frames captured")
})
}
@objc
private func takeSnapshot() {
frameCount = 1
let render = UIGraphicsImageRenderer(size: selectedView.bounds.size)
let _ = render.image { (context) in
selectedView.drawHierarchy(in: selectedView.bounds, afterScreenUpdates: true)
}
}
which results in
Start time 2022-07-01 04:20:01 0000
Finished at 2022-07-01 04:20:07 0000 with 180 frames captured
Decreasing the timeInterval
to 0.01
yields
Start time 2022-07-01 04:25:30 0000
Finished at 2022-07-01 04:25:36 0000 with 506 frames captured
CPU usage peaked around 20% during this time and the app did not lag at all in my trivial example.
CodePudding user response:
Rather than taking the snapshot I thing use generator is a good option here it takes cpu but not memory which cause smoother ui/ux
class VideoController:UIViewController {
var timer = Timer()
var secs:Double = 0
var urls = [URL?]() {
didSet {
print(urls.count)
}
}
override func viewDidLoad() {
super.viewDidLoad()
everSecond()
}
func everSecond() {
// every second trigger
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(generate), userInfo: nil, repeats: true)
}
@objc
func generate() {
let url = Bundle.main.url(forResource: "Collection", withExtension: "mov")
let videoAsset = AVAsset(url: url!)
let duration = videoAsset.duration.seconds // duration of video
if secs < duration {
var timesArray:[NSValue] = []
for i in 1...30 {
let value:Double = Double(31 - i) // invers
let t = CMTime(value: CMTimeValue(1 / value) Int64(secs), timescale: 1) // getting time
timesArray.append(NSValue(time: t)) // appending array
}
let generator = AVAssetImageGenerator(asset: videoAsset)
generator.requestedTimeToleranceBefore = .zero
//Optional generator.requestedTimeToleranceAfter = .zero //Optional
generator.generateCGImagesAsynchronously(forTimes: timesArray) { requestedTime, image, actualTime, result, error in
if let img = image {
let m = UIImage(cgImage: img) // uiimage
let url = ViewController.saveImageInDocumentDirectory(image: m, fileName: "\(Date().timeStamp()).jpeg") //saved in documents with unique name
self.urls.append(url) // append url to array urls for save refrecnce
}
}
secs = 1
} else {
timer.invalidate()
}
}
public static func saveImageInDocumentDirectory(image: UIImage, fileName: String) -> URL? {
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!;
let fileURL = documentsUrl.appendingPathComponent(fileName)
if let imageData = image.pngData() {
try? imageData.write(to: fileURL, options: .atomic)
return fileURL
}
return nil
}
public static func loadImageFromDocumentDirectory(fileName: String) -> UIImage? {
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!;
let fileURL = documentsUrl.appendingPathComponent(fileName)
do {
let imageData = try Data(contentsOf: fileURL)
return UIImage(data: imageData)
} catch {}
return nil
}
}
extension Date {
func timeStamp() -> String {
let fomatter = DateFormatter()
fomatter.dateFormat = "yyyy-MM-dd HH:mm:ss:SSS"
return fomatter.string(from: self)
}
}