AVExportSession | Add UIView with GIFs and drawings on a video

840 Views Asked by At

Objective : I have a Video over which I have a UIView which contains animated GIFs(not locally stored, but using giphy api), Texts, or hand drawings. I want to export this along with the image in a single video.

What I did : I created a UIView on which the animations are. Then converted that to CALayer and added to video with AVMutableVideoCompotion.

Problem : The UIView with animations is being converted to an Image instead of a video. How can I solve this. Below is the Program for my export session. Any pointers will be really helpful.

func convertVideoAndSaveTophotoLibrary(videoURL: URL) {

    let file = FileManager.shared.getDocumentDirectory(path: currentFilename)
    FileManager.shared.clearPreviousFiles(withPath: file.path)

    // File to composit
    let asset = AVURLAsset(url: videoURL as URL)
    let composition = AVMutableComposition.init()
    composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid)
    let clipVideoTrack = asset.tracks(withMediaType: AVMediaType.video)[0]

    // Rotate to potrait
    let transformer = AVMutableVideoCompositionLayerInstruction(assetTrack: clipVideoTrack)
    let videoTransform:CGAffineTransform = clipVideoTrack.preferredTransform

    //fix orientation
    var videoAssetOrientation_  = UIImage.Orientation.up

    var isVideoAssetPortrait_  = false

    if videoTransform.a == 0 && videoTransform.b == 1.0 && videoTransform.c == -1.0 && videoTransform.d == 0 {
        videoAssetOrientation_ = UIImage.Orientation.right
        isVideoAssetPortrait_ = true
    }
    if videoTransform.a == 0 && videoTransform.b == -1.0 && videoTransform.c == 1.0 && videoTransform.d == 0 {
        videoAssetOrientation_ =  UIImage.Orientation.left
        isVideoAssetPortrait_ = true
    }
    if videoTransform.a == 1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == 1.0 {
        videoAssetOrientation_ =  UIImage.Orientation.up
    }
    if videoTransform.a == -1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == -1.0 {
        videoAssetOrientation_ = UIImage.Orientation.down;
    }


    transformer.setTransform(clipVideoTrack.preferredTransform, at: CMTime.zero)
    transformer.setOpacity(0.0, at: asset.duration)

    //adjust the render size if neccessary
    var naturalSize: CGSize
    if(isVideoAssetPortrait_){
        naturalSize = CGSize(width: clipVideoTrack.naturalSize.height, height: clipVideoTrack.naturalSize.width)
    } else {
        naturalSize = clipVideoTrack.naturalSize;
    }

    var renderWidth: CGFloat!
    var renderHeight: CGFloat!

    renderWidth = naturalSize.width
    renderHeight = naturalSize.height

    let parentlayer = CALayer()
    let videoLayer = CALayer()
    let watermarkLayer = CALayer()


    let videoComposition = AVMutableVideoComposition()
    videoComposition.renderSize = CGSize(width: renderWidth, height: renderHeight)
    videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
    videoComposition.renderScale = 1.0
    
    //---------------------->>>>>> converting uiview to uiimage
    watermarkLayer.contents = canvasView.asImage().cgImage

    parentlayer.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: naturalSize)
    videoLayer.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: naturalSize)
    watermarkLayer.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: naturalSize)

    parentlayer.addSublayer(videoLayer)
    parentlayer.addSublayer(watermarkLayer)

    //---------------------->>>>>> Add view to video
    videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayers: [videoLayer], in: parentlayer)

    let instruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: CMTimeMakeWithSeconds(60, preferredTimescale: 30))

    instruction.layerInstructions = [transformer]
    videoComposition.instructions = [instruction]

    let exporter = AVAssetExportSession.init(asset: asset, presetName: AVAssetExportPresetHighestQuality)
    exporter?.outputFileType = AVFileType.mp4
    exporter?.outputURL = file
    exporter?.videoComposition = videoComposition
    exporter?.shouldOptimizeForNetworkUse = true
    
    exporter!.exportAsynchronously(completionHandler: {() -> Void in
        if exporter?.status == .completed {
            let outputURL: URL? = exporter?.outputURL
            self.saveToPhotoLibrary(url: outputURL!)
        }
    })
}

Converting UIView to UIimage

extension UIView {
func asImage() -> UIImage {
    let renderer = UIGraphicsImageRenderer(bounds: bounds)
    return renderer.image { rendererContext in
        layer.render(in: rendererContext.cgContext)
    }
}

}

Code for Adding the GIF(I am using the Giphy API here), so the gif is downloaded and then added

func didSelectMedia(giphyViewController: GiphyViewController, media: GPHMedia) {
    addMedia(media: media)
    giphyViewController.dismiss(animated: true) { [weak self] in
        self?.giphy = nil
    }
}

// GPHMediaView is a subclass of UIImageView
func addMedia(media: GPHMedia) {
    let mediaView = GPHMediaView()
    mediaView.media = media
    mediaView.contentMode = .scaleAspectFill
    mediaView.frame.size = CGSize(width: 150, height: 150)
    mediaView.center = canvasView.center
    canvasView.addSubview(mediaView)
    print(mediaView.frame)
    self.addGesturesTo(mediaView)
}

What I am getting: The cat over the video is a gif. But sadly all i get is one frame. Now I know that is because I am converting the view to image. But that's the solution I need to know. How do I have the gif merged to the video.

enter image description here

1

There are 1 best solutions below

0
Stas Klukhin On

You have two ways to archive this. First you can convert gif to video and add it to composition, but you lose alpha channel. Second way and more relevant is to add CAKeyframeAnimation on gif layer. To do this you should get all image frames from gif and put it all to key CAKeyframeAnimation.values and set duration which equal to framesCount * framesPerSecond.

class func makeContentAnimation(beginTime: Double, values: [Any], frameRate: Double) -> CAKeyframeAnimation {
    let animation = CAKeyframeAnimation(keyPath: "contents")
    animation.values = values
    animation.beginTime = beginTime.isZero ? AVCoreAnimationBeginTimeAtZero : beginTime
    animation.duration = frameRate * Double(values.count)
    animation.isRemovedOnCompletion = false
    animation.repeatCount = .infinity
    return animation
}