使用 AVAssetWriter 将图像转换为视频期间的 iOS 快速内存问题

问题描述

我已经为此做了很多搜索并做了很多实验,但我没有得到任何合适的解决方案。

我尝试将 UIImages 转换为视频。我有 250 多个图像阵列,我尝试将这些图像转换为 60FPS 的视频。

我将渲染代码放在 autoreleasepool 方法中并添加了一些其他代码也添加了 autoreleasepool 但没有效果。

代码。

import AVFoundation
import UIKit
import Photos
import AVKit

var tempurl = ""

struct RenderSettings {
    
    var width: CGFloat = UIScreen.main.bounds.width * UIScreen.main.scale
    var height: CGFloat = UIScreen.main.bounds.width * UIScreen.main.scale
    var fps: Int32 = 60   //frames per second
    var avCodecKey = AVVideoCodecType.h264
    var videoFilename = "ImageToVideo"
    var videoFilenameExt = "mp4"
    
    var size: CGSize {
        return CGSize(width: width,height: height)
    }
    
    var outputURL: URL {
        
        let fileManager = FileManager.default
        if let tmpDirURL = try? fileManager.url(for: .cachesDirectory,in: .userDomainMask,appropriateFor: nil,create: true) {
            return tmpDirURL.appendingPathComponent(videoFilename).appendingPathExtension(videoFilenameExt) as URL
        }
        fatalError("URLForDirectory() failed")
        
    }
}

class VideoWriter {
    
    let renderSettings: RenderSettings
    
    var videoWriter: AVAssetWriter!
    var videoWriterInput: AVAssetWriterInput!
    var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor!
    
    var isReadyForData: Bool {
        return videoWriterInput?.isReadyForMoreMediaData ?? false
    }
    
    class func pixelBufferFromImage(image: UIImage,pixelBufferPool: CVPixelBufferPool,size: CGSize) -> CVPixelBuffer {
        
        autoreleasepool {
            
            var pixelBufferOut: CVPixelBuffer?
            
            let status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault,pixelBufferPool,&pixelBufferOut)
            
            if status != kCVReturnSuccess {
                fatalError("CVPixelBufferPoolCreatePixelBuffer() failed")
            }
            
            let pixelBuffer = pixelBufferOut!
            
            CVPixelBufferLockBaseAddress(pixelBuffer,[])
            
            let data = CVPixelBufferGetBaseAddress(pixelBuffer)
            let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
            let context = CGContext(data: data,width: Int(size.width),height: Int(size.height),bitsPerComponent: 8,bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer),space: rgbColorSpace,bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)
            
            context!.clear(CGRect(x: 0,y: 0,width: size.width,height: size.height))
            
            let horizontalRatio = size.width / image.size.width
            let verticalRatio = size.height / image.size.height
            
            let aspectRatio = min(horizontalRatio,verticalRatio) // ScaleAspectFit
            
            let newSize = CGSize(width: image.size.width * aspectRatio,height: image.size.height * aspectRatio)
            
            let x = newSize.width < size.width ? (size.width - newSize.width) / 2 : 0
            let y = newSize.height < size.height ? (size.height - newSize.height) / 2 : 0
            
            context!.concatenate(CGAffineTransform.identity)
            context!.draw(image.cgImage!,in: CGRect(x: x,y: y,width: newSize.width,height: newSize.height))
            
            CVPixelBufferUnlockBaseAddress(pixelBuffer,[])
            
            return pixelBuffer
        }
    }
    
    init(renderSettings: RenderSettings) {
        self.renderSettings = renderSettings
    }
    
    func start() {
        
        let avOutputSettings: [String: AnyObject] = [
            AVVideoCodecKey: renderSettings.avCodecKey as AnyObject,AVVideoWidthKey: NSNumber(value: Float(renderSettings.width)),AVVideoHeightKey: NSNumber(value: Float(renderSettings.height))
        ]
        
        func createPixelBufferAdaptor() {
            let sourcePixelBufferAttributesDictionary = [
                kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32ARGB),kCVPixelBufferWidthKey as String: NSNumber(value: Float(renderSettings.width)),kCVPixelBufferHeightKey as String: NSNumber(value: Float(renderSettings.height))
            ]
            pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput,sourcePixelBufferAttributes: sourcePixelBufferAttributesDictionary)
        }
        
        func createAssetWriter(outputURL: URL) -> AVAssetWriter {
            guard let assetWriter = try? AVAssetWriter(outputURL: outputURL,fileType: AVFileType.mp4) else {
                fatalError("AVAssetWriter() failed")
            }
            
            guard assetWriter.canApply(outputSettings: avOutputSettings,forMediaType: AVMediaType.video) else {
                fatalError("canApplyOutputSettings() failed")
            }
            
            return assetWriter
        }
        
        videoWriter = createAssetWriter(outputURL: renderSettings.outputURL)
        videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video,outputSettings: avOutputSettings)
        
        if videoWriter.canAdd(videoWriterInput) {
            videoWriter.add(videoWriterInput)
        }
        else {
            fatalError("canAddInput() returned false")
        }
        
        
        createPixelBufferAdaptor()
        
        if videoWriter.startWriting() == false {
            fatalError("startWriting() failed")
        }
        
        videoWriter.startSession(atSourceTime: CMTime.zero)
        
        precondition(pixelBufferAdaptor.pixelBufferPool != nil,"nil pixelBufferPool")
    }
    
    
    func render(appendPixelBuffers: @escaping (VideoWriter)->Bool,completion: @escaping ()->Void) {

        autoreleasepool {
            precondition(videoWriter != nil,"Call start() to initialze the writer")

            let queue = DispatchQueue(label: "mediaInputQueue")
            videoWriterInput.requestMediaDataWhenReady(on: queue) {
                let isFinished = appendPixelBuffers(self)
                if isFinished {
                    self.videoWriterInput.markAsFinished()
                    self.videoWriter.finishWriting() {
                        DispatchQueue.main.async {
                            completion()
                        }
                    }
                }
            }
        }
        
    }
    
    func addImage(image: UIImage,withPresentationTime presentationTime: CMTime) -> Bool {
        
        autoreleasepool {
            precondition(pixelBufferAdaptor != nil,"Call start() to initialze the writer")
            
            let pixelBuffer = VideoWriter.pixelBufferFromImage(image: image,pixelBufferPool: pixelBufferAdaptor.pixelBufferPool!,size: renderSettings.size)
            return pixelBufferAdaptor.append(pixelBuffer,withPresentationTime: presentationTime)
        }
            
    }
    
}

class ImageAnimator {
    
    static let kTimescale: Int32 = 600
    
    let settings: RenderSettings
    let videoWriter: VideoWriter
    var images: [UIImage]!
    
    var frameNum = 0
    
    class func removeFileAtURL(fileURL: URL) {
        do {
            try FileManager.default.removeItem(atPath: fileURL.path)
        }
        catch _ as NSError {
            //
        }
    }
    
    init(renderSettings: RenderSettings,imagearr: [UIImage]) {
        settings = renderSettings
        videoWriter = VideoWriter(renderSettings: settings)
        images = imagearr
    }
    
    func render(completion: @escaping ()->Void) {
        
        // The VideoWriter will fail if a file exists at the URL,so clear it out first.
        ImageAnimator.removeFileAtURL(fileURL: settings.outputURL)
        
        videoWriter.start()
        videoWriter.render(appendPixelBuffers: appendPixelBuffers) {
            let s: String = self.settings.outputURL.path
            tempurl = s
            completion()
        }
        
    }
    
    func appendPixelBuffers(writer: VideoWriter) -> Bool {
        
        let frameDuration = CMTimeMake(value: Int64(ImageAnimator.kTimescale / settings.fps),timescale: ImageAnimator.kTimescale)
        
        
        while !images.isEmpty {
            
            if writer.isReadyForData == false {
                return false
            }
            
            let image = images.removeFirst()
            let presentationTime = CMTimeMultiply(frameDuration,multiplier: Int32(frameNum))
            let success = videoWriter.addImage(image: image,withPresentationTime: presentationTime)
            if success == false {
                fatalError("addImage() failed")
            }
            
            frameNum=frameNum+1
        }
        
        return true
    }
    
}

内存使用:

enter image description here

使用此代码获取图像:

   @objc public class Recorder: NSObject {
    
    public var view : UIView?
    
    var displayLink : CADisplayLink?
    var referenceDate : NSDate?
    var imageArray = [UIImage]()
    
    public func start() {
        
        self.imageArray.removeAll()
        
        if (view == nil) {
            NSException(name: NSExceptionName(rawValue: "No view set"),reason: "You must set a view before calling start.",userInfo: nil).raise()
        }else {
            displayLink = CADisplayLink(target: self,selector: #selector(self.handleDisplayLink(displayLink:)))
            displayLink!.add(to: RunLoop.main,forMode: RunLoop.Mode.common)
            referenceDate = NSDate()
        }
        
    }
    
    @objc func handleDisplayLink(displayLink : CADisplayLink) {
        if (view != nil) {
            createImageFromView(captureView: view!)
        }
    }

    
    func createImageFromView(captureView : UIView) {
        
        UIGraphicsBeginImageContextWithOptions(captureView.bounds.size,false,0)
        captureView.drawHierarchy(in: captureView.bounds,afterScreenUpdates: false)
        
        let image = UIGraphicsGetImageFromCurrentImageContext();
        
        if let img = image {
            self.imageArray.append(img)
        }

        UIGraphicsEndImageContext();
    }
    
    public func stop(completion: @escaping (_ saveURL: String) -> Void) {
        
        displayLink?.invalidate()
        
        let seconds = referenceDate?.timeIntervalSinceNow
        if (seconds != nil) {
            
            print("Image Count : \(self.imageArray.count)")            
            DispatchQueue.main.async {
                let settings = RenderSettings()
                let imageAnimator = ImageAnimator(renderSettings: settings,imagearr: self.imageArray)
                imageAnimator.render() {
                    let u: String = tempurl
                    completion(u)
                    //self.saveVideoInPhotos()
                }
            }
            
        }
        
    }    
    
}

提前致谢

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...