Search code examples
iosswiftswiftuicvpixelbufferavassetwriterinput

swift CVPixelBuffer video is darker than original image


My app takes a snapshot of view: ZStack ( image with opacity 0.4 , white rectangle with opacity 0.25 then text) and save it as a image then enables user to generate a video using that image and some audio, I followed tutorials

https://img.ly/blog/how-to-make-videos-from-still-images-with-avfoundation-and-swift/ http://twocentstudios.com/2017/02/20/creating-a-movie-with-an-image-and-audio-on-ios/ https://www.hackingwithswift.com/quick-start/swiftui/how-to-convert-a-swiftui-view-to-an-image

The video is generated successfully with audio and image, however the output video is always darker than the produced image from snapshot.

saved image and video from photos app

image looks like what appear in device:

photo

output video darker than the used image :S

video

here are some functions

snapshot

extension View {
    func snapshot() -> UIImage {
        let controller = UIHostingController(rootView: self.ignoresSafeArea(.all))
        let view = controller.view

    let targetSize = controller.view.intrinsicContentSize 
        view?.bounds = CGRect(origin: .zero, size: targetSize)
        view?.backgroundColor = .clear
    
        let renderer = UIGraphicsImageRenderer(size: targetSize)

        return renderer.image { _ in
            view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
        }
        
    }
    
}

Create video using image

  func writeSingleImageToMovie(image: UIImage, movieLength: TimeInterval, outputFileURL: URL, completion: @escaping (Error?) -> ())
     {
        print("writeSingleImageToMovie is called")
   
     
     
        do {
            let imageSize = image.size
            let videoWriter = try AVAssetWriter(outputURL: outputFileURL, fileType: AVFileType.mp4)
            let videoSettings: [String: Any] = [AVVideoCodecKey: AVVideoCodecType.h264,
                                                AVVideoWidthKey: imageSize.width, //was imageSize.width
                                                AVVideoHeightKey: imageSize.height] //was imageSize.height
            
          
            let videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings)
            let adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput, sourcePixelBufferAttributes: nil)
            
            if !videoWriter.canAdd(videoWriterInput) { throw NSError() }
            videoWriterInput.expectsMediaDataInRealTime = true
            videoWriter.add(videoWriterInput)
            
            videoWriter.startWriting()
            let timeScale: Int32 = 4 // 600 recommended in CMTime for movies.
            let halfMovieLength = Float64(movieLength/2.0) // videoWriter assumes frame lengths are equal.
            let startFrameTime = CMTimeMake(value: 0, timescale: timeScale)
         
            
            let endFrameTime = CMTimeMakeWithSeconds(Double(60), preferredTimescale: timeScale) 
                                                  
            videoWriter.startSession(atSourceTime: startFrameTime)
          
         
            
        guard let cgImage = image.cgImage else { throw NSError() }
             
     
            let buffer: CVPixelBuffer = try CGImage.pixelBuffer(fromImage: cgImage, size: imageSize)

            while !adaptor.assetWriterInput.isReadyForMoreMediaData { usleep(10) }
            adaptor.append(buffer, withPresentationTime: startFrameTime)
            while !adaptor.assetWriterInput.isReadyForMoreMediaData { usleep(10) }
            adaptor.append(buffer, withPresentationTime: endFrameTime)
            
            videoWriterInput.markAsFinished()
            videoWriter.finishWriting
            {
                completion(videoWriter.error)
            }
            
        } catch {
            print("CATCH Error in writeSingleImageToMovie")
            completion(error)
        }
}

Here is the function to create CVPixelBuffer, I tried to create buffer using CIImage also but got the same result

extension CGImage {
 
    static func pixelBuffer(fromImage image: CGImage, size: CGSize) throws -> CVPixelBuffer {
        print("pixelBuffer from CGImage")
        let options: CFDictionary = [kCVPixelBufferCGImageCompatibilityKey as String: true, kCVPixelBufferCGBitmapContextCompatibilityKey as String: true] as CFDictionary
        var pxbuffer: CVPixelBuffer? = nil
        let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width), Int(size.height), kCVPixelFormatType_32ARGB, options, &pxbuffer) 
        guard let buffer = pxbuffer, status == kCVReturnSuccess else { throw NSError() }
        
        CVPixelBufferLockBaseAddress(buffer, [])
        guard let pxdata = CVPixelBufferGetBaseAddress(buffer)
        else { throw NSError() }
        let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
        
        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
         
            guard let context = CGContext(data: pxdata, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) else { print("error in `CG context")
                throw NSError() }
        context.concatenate(CGAffineTransform(rotationAngle: 0))
        context.draw(image, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
        
        CVPixelBufferUnlockBaseAddress(buffer, [])
        
        return buffer
        }
}

I got stuck into this problem, I can't seem to find a solution.. any hint would be appreciated.

I created mini showcase app: https://github.com/digitallegend/view2video


Solution

  • It appears you are generating the image based on a black (or nil) background...

    In your ShareVideoSheet file, set the VStack background to white:

    var imageTextBackground: some View {
       
        ZStack {
    
            // everything as you have it...
    
        }
        .background(Color.white)
    
    }
    

    That should fix the "dark video output" issue.

    Alternatively, in your:

    extension View {
        func snapshot() -> UIImage {
    

    change:

        view?.backgroundColor = .clear
    

    to:

        view?.backgroundColor = .white