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:
output video darker than the used image :S
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
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