swiftxcodeavfoundationvideo-recordingavcapturemoviefileoutput

How do you add an overlay while recording a video in Swift?


I am trying to record, and then save, a video in Swift using AVFoundation. This works. I am also trying to add an overlay, such as a text label containing the date, to the video.

For example: the video saved is not only what the camera sees, but the timestamp as well.

Here is how I am saving the video:

func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
    saveVideo(toURL: movieURL!)
  }

  private func saveVideo(toURL url: URL) {
    PHPhotoLibrary.shared().performChanges({
      PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)
    }) { (success, error) in
      if(success) {
        print("Video saved to Camera Roll.")
      } else {
        print("Video failed to save.")
      }
    }
  }

I have a movieOuput that is an AVCaptureMovieFileOutput. My preview layer does not contain any sublayers. I tried adding the timestamp label's layer to the previewLayer, but this did not succeed.

I have tried Ray Wenderlich's example as well as this stack overflow question. Lastly, I also tried this tutorial, all of which to no avail.

How can I add an overlay to my video that is in the saved video in the camera roll?


Solution

  • Without more information it sounds like what you are asking for is a WATERMARK. Not an overlay.

    A watermark is a markup on the video that will be saved with the video. An overlay is generally showed as subviews on the preview layer and will not be saved with the video.

    Check this out here: https://stackoverflow.com/a/47742108/8272698

    func addWatermark(inputURL: URL, outputURL: URL, handler:@escaping (_ exportSession: AVAssetExportSession?)-> Void) {
        let mixComposition = AVMutableComposition()
        let asset = AVAsset(url: inputURL)
        let videoTrack = asset.tracks(withMediaType: AVMediaType.video)[0]
        let timerange = CMTimeRangeMake(kCMTimeZero, asset.duration)
    
            let compositionVideoTrack:AVMutableCompositionTrack = mixComposition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid))!
    
        do {
            try compositionVideoTrack.insertTimeRange(timerange, of: videoTrack, at: kCMTimeZero)
            compositionVideoTrack.preferredTransform = videoTrack.preferredTransform
        } catch {
            print(error)
        }
    
        let watermarkFilter = CIFilter(name: "CISourceOverCompositing")!
        let watermarkImage = CIImage(image: UIImage(named: "waterMark")!)
        let videoComposition = AVVideoComposition(asset: asset) { (filteringRequest) in
            let source = filteringRequest.sourceImage.clampedToExtent()
            watermarkFilter.setValue(source, forKey: "inputBackgroundImage")
            let transform = CGAffineTransform(translationX: filteringRequest.sourceImage.extent.width - (watermarkImage?.extent.width)! - 2, y: 0)
            watermarkFilter.setValue(watermarkImage?.transformed(by: transform), forKey: "inputImage")
            filteringRequest.finish(with: watermarkFilter.outputImage!, context: nil)
        }
    
        guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) else {
            handler(nil)
    
            return
        }
    
        exportSession.outputURL = outputURL
        exportSession.outputFileType = AVFileType.mp4
        exportSession.shouldOptimizeForNetworkUse = true
        exportSession.videoComposition = videoComposition
        exportSession.exportAsynchronously { () -> Void in
            handler(exportSession)
        }
    }
    

    And heres how to call the function.

    let outputURL = NSURL.fileURL(withPath: "TempPath")
    let inputURL = NSURL.fileURL(withPath: "VideoWithWatermarkPath")
    addWatermark(inputURL: inputURL, outputURL: outputURL, handler: { (exportSession) in
        guard let session = exportSession else {
            // Error 
            return
        }
        switch session.status {
            case .completed:
            guard NSData(contentsOf: outputURL) != nil else {
                // Error
                return
            }
    
            // Now you can find the video with the watermark in the location outputURL
    
            default:
            // Error
        }
    })
    

    Let me know if this code works for you. It is in swift 3 so some changes will be needed. I currently am using this code on an app of mine. Have not updated it to swift 5 yet