Skip to content

Instantly share code, notes, and snippets.

@levantAJ
Last active May 6, 2025 09:20

Revisions

  1. levantAJ revised this gist May 3, 2017. No changes.
  2. levantAJ revised this gist May 3, 2017. 1 changed file with 24 additions and 24 deletions.
    48 changes: 24 additions & 24 deletions AssetRecorderView.swift
    Original file line number Diff line number Diff line change
    @@ -76,7 +76,7 @@ final class AssetRecorderView: UIView {
    fileprivate let maskGenerator = AssetRecorderMaskGenerator()

    fileprivate let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])!
    fileprivate let videoProcessingQueue = DispatchQueue(label: "com.hilaoinc.hilao.queue.asset-recorder.video-output-processing")
    fileprivate let videoProcessingQueue = DispatchQueue(label: "com.hilaoinc.hilao.queue.asset-recorder.video-output-processing", qos: .userInitiated)

    fileprivate var previewLayer: AVCaptureVideoPreviewLayer!

    @@ -251,11 +251,11 @@ extension AssetRecorderView {
    addDeviceInputs()

    videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,]
    videoDataOutput.alwaysDiscardsLateVideoFrames = true
    if captureSession.canAddOutput(videoDataOutput) {
    let videoOutputQueue = DispatchQueue(label: "com.hilaoinc.hilao.queue.record-video.video-output")
    videoDataOutput.setSampleBufferDelegate(self, queue: videoOutputQueue)
    captureSession.addOutput(videoDataOutput)
    videoDataOutput.alwaysDiscardsLateVideoFrames = true
    }

    if captureSession.canAddOutput(audioDataOutput) {
    @@ -348,31 +348,34 @@ extension AssetRecorderView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCap
    sampleBuffer != nil,
    connection != nil else { return }
    if captureOutput == videoDataOutput {
    connection.videoOrientation = AVCaptureVideoOrientation(orientation: UIDevice.current.orientation) ?? .portrait
    var features: [CIFaceFeature] = []
    if maskType == .none {
    features = []
    } else {
    let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
    let attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault, pixelBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate)) as? [String: Any]
    let ciImage = CIImage(cvImageBuffer: pixelBuffer, options: attachments)

    let fs = faceDetector.features(in: ciImage, options: [
    CIDetectorSmile: true,
    CIDetectorEyeBlink: true,
    ])
    features = fs.flatMap { $0 as? CIFaceFeature }
    }

    videoProcessingQueue.async { [weak self] in
    guard let strongSelf = self else { return }
    var features: [CIFaceFeature] = []
    connection.videoOrientation = AVCaptureVideoOrientation(orientation: UIDevice.current.orientation) ?? .portrait

    if strongSelf.maskType == .none {
    features = []
    } else {
    let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
    let attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault, pixelBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate)) as? [String: Any]
    let ciImage = CIImage(cvImageBuffer: pixelBuffer, options: attachments)

    let fs = strongSelf.faceDetector.features(in: ciImage, options: [
    CIDetectorSmile: true,
    CIDetectorEyeBlink: true,
    ])
    features = fs.flatMap { $0 as? CIFaceFeature }
    }
    strongSelf.recorderWriter.handle(video: sampleBuffer, features: features, maskType: strongSelf.maskType)
    let desc = CMSampleBufferGetFormatDescription(sampleBuffer)!
    let bufferFrame = CMVideoFormatDescriptionGetCleanAperture(desc, false)
    strongSelf.maskGenerator.drawFaceMasksFor(features: features, bufferFrame: bufferFrame, maskType: strongSelf.maskType, layer: strongSelf.layer, frame: strongSelf.frame)
    }

    let desc = CMSampleBufferGetFormatDescription(sampleBuffer)!
    let bufferFrame = CMVideoFormatDescriptionGetCleanAperture(desc, false)
    maskGenerator.drawFaceMasksFor(features: features, bufferFrame: bufferFrame, maskType: maskType, layer: layer, frame: frame)
    debugPrint("video...")
    } else if captureOutput == audioDataOutput {
    recorderWriter.handle(audio: sampleBuffer)
    debugPrint("audio...")
    }
    }
    }
    @@ -425,7 +428,6 @@ extension Error {
    }
    }


    //
    // AssetRecorderWriter.swift
    // Hilao
    @@ -499,7 +501,6 @@ extension AssetRecorderWriter {
    }

    func handle(audio sampleBuffer: CMSampleBuffer) {
    startWritingIfNeeded(sampleBuffer: sampleBuffer)
    guard canWriting() else { return }
    defer {
    set(currentTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
    @@ -675,7 +676,6 @@ extension AssetRecorderWriter {
    }
    }


    //
    // AssetRecorderMaskGenerator.swift
    // Hilao
  3. levantAJ created this gist May 3, 2017.
    986 changes: 986 additions & 0 deletions AssetRecorderView.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,986 @@
    //
    // AssetRecorderView.swift
    // Snowball
    //
    // Created by Le Tai on 7/20/16.
    // Copyright © 2016 Snowball. All rights reserved.
    //

    import UIKit
    import AVFoundation
    import ImageIO

    enum AssetRecorderViewType {
    case image
    case video
    }

    protocol AssetRecorderViewDelegate: class {
    func assetRecorderView(_ view: AssetRecorderView, didExport videoAssetURL: URL, userInfo: [String: Any]?)
    func assetRecorderView(_ view: AssetRecorderView, didCapture image: UIImage)
    func assetRecorderView(_ view: AssetRecorderView, didTurnOnLight isLightOn: Bool)
    func assetRecorderView(_ view: AssetRecorderView, didChangeCamera isFrontFacingCamera: Bool)
    func assetRecorderView(_ view: AssetRecorderView, didRecord isRecording: Bool)
    func assetRecorderView(_ view: AssetRecorderView, didFail error: Error, userInfo: [String: Any]?)
    }

    extension AssetRecorderViewDelegate {
    func assetRecorderView(_ view: AssetRecorderView, didExport videoAssetURL: URL, userInfo: [String: Any]?) {}
    func assetRecorderView(_ view: AssetRecorderView, didCapture image: UIImage) {}
    func assetRecorderView(_ view: AssetRecorderView, didTurnOnLight isLightOn: Bool) {}
    func assetRecorderView(_ view: AssetRecorderView, didChangeCamera isFrontFacingCamera: Bool) {}
    func assetRecorderView(_ view: AssetRecorderView, didRecord isRecording: Bool) {}
    func assetRecorderView(_ view: AssetRecorderView, didFail error: Error, userInfo: [String: Any]?) {}
    }

    final class AssetRecorderView: UIView {
    static let shared = AssetRecorderView()

    var type: AssetRecorderViewType = .video
    var maskType: MaskType = .none {
    didSet {
    guard isRecording else { return }
    recordingMasks.append(maskType)
    }
    }
    var lightOn: Bool = false {
    didSet {
    changeLight()
    delegate?.assetRecorderView(self, didTurnOnLight: lightOn)
    }
    }
    var isUsingFrontFacingCamera = true {
    didSet {
    captureSession.beginConfiguration()
    addDeviceInputs()
    captureSession.commitConfiguration()
    if !captureSession.isRunning {
    captureSession.startRunning()
    }
    delegate?.assetRecorderView(self, didChangeCamera: isUsingFrontFacingCamera)
    }
    }

    weak var delegate: AssetRecorderViewDelegate?

    fileprivate(set) lazy var isExporting = false

    fileprivate let captureSession = AVCaptureSession()
    fileprivate var videoDevice: AVCaptureDevice!
    fileprivate var videoInput: AVCaptureDeviceInput!
    fileprivate lazy var videoDataOutput = AVCaptureVideoDataOutput()
    fileprivate lazy var audioDataOutput = AVCaptureAudioDataOutput()
    fileprivate lazy var stillImageOutput = AVCaptureStillImageOutput()

    fileprivate var recorderWriter = AssetRecorderWriter()
    fileprivate let maskGenerator = AssetRecorderMaskGenerator()

    fileprivate let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])!
    fileprivate let videoProcessingQueue = DispatchQueue(label: "com.hilaoinc.hilao.queue.asset-recorder.video-output-processing")

    fileprivate var previewLayer: AVCaptureVideoPreviewLayer!

    fileprivate lazy var recordingMasks: [MaskType] = []

    required init() {
    super.init(frame: .zero)
    commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    }

    override func layoutSubviews() {
    super.layoutSubviews()
    relayout()
    }
    }

    // MARK: - Getters & Setters

    extension AssetRecorderView {
    var isPausing: Bool {
    return recorderWriter.isPausing
    }

    var isRecording: Bool {
    return recorderWriter.isRecording
    }

    func run() {
    guard !captureSession.isRunning else { return }
    DispatchQueue(label: "com.hilaoinc.hilao.queue.asset-recorder-view.run").async { [weak self] in
    self?.captureSession.startRunning()
    }
    }

    func relayout() {
    previewLayer?.setAffineTransform(.identity)
    previewLayer?.frame = bounds
    }

    func prepare() {
    guard videoDevice == nil else { return }
    setupRecorder()
    }

    func start() {
    recordingMasks.replace(maskType)
    recorderWriter = AssetRecorderWriter()
    recorderWriter.delegate = self
    recorderWriter.start()
    }

    func pause() {
    recorderWriter.pause()
    }

    func resume() {
    recordingMasks.replace(maskType)
    recorderWriter.resume()
    }

    func stop(_ completion: @escaping (Response<URL>) -> Void) {
    log(masks: recordingMasks)
    recordingMasks.removeAll()
    recorderWriter.stop(completion)
    }

    func export(userInfo: [String: Any]? = nil, readyToRecord: (() -> Void)? = nil) {
    guard !isExporting else { return }
    isExporting = true
    DispatchQueue(label: "com.hilao.hilao.queue.asset-recorder-view.export").async { [unowned self] in
    self.stop { [unowned self] response in
    switch response {
    case .failure(let error):
    self.isExporting = false
    TrackingManager.log(event: .recordAssetUnknownError)
    self.throw(error: error, userInfo: userInfo)
    case .success(let outputURL):
    let asset = AVURLAsset(url: outputURL)
    debugPrint("Record asset", asset.duration.seconds)
    var timeRange: CMTimeRange? = nil
    let shakeDetectingTime = Constant.ShakingManager.ShakeDetectingTime
    if let isShaked = userInfo?[Constant.AssetRecorderView.IsShakedKey] as? Bool,
    isShaked == true {
    if asset.duration.seconds > shakeDetectingTime {
    timeRange = CMTimeRange(start: kCMTimeZero, duration: CMTime(seconds: asset.duration.seconds - shakeDetectingTime))
    } else {
    self.isExporting = false
    self.throwAssetTooShortError(userInfo: userInfo)
    return
    }
    }
    if let duration = timeRange?.duration.seconds {
    if let minimumDuration = userInfo?[Constant.AssetRecorderView.MinimumDurationKey] as? Int,
    duration < Double(minimumDuration) {
    self.isExporting = false
    self.throwAssetTooShortError(userInfo: userInfo)
    return
    }
    if let maximumDuration = userInfo?[Constant.AssetRecorderView.MaximumDurationKey] as? Int,
    duration > Double(maximumDuration) {
    timeRange = CMTimeRange(start: timeRange!.start, duration: CMTime(seconds: Double(maximumDuration)))
    TrackingManager.log(event: .submitToLongReactionVideo)
    }
    }
    AssetUtils.export(asset: asset, timeRange: timeRange, completion: { [unowned self] response in
    DispatchQueue.main.async { [unowned self] in
    self.isExporting = false
    switch response {
    case .failure(let error):
    self.throw(error: error, userInfo: userInfo)
    case .success(let url):
    self.delegate?.assetRecorderView(self, didExport: url, userInfo: userInfo)
    }
    }
    })
    }

    DispatchQueue.main.async { unowned in
    readyToRecord?()
    }
    }
    }
    }

    func caturePhoto() {
    guard !isExporting,
    let videoConnection = stillImageOutput.connection(withMediaType: AVMediaTypeVideo) else { return }
    isExporting = true
    stillImageOutput.captureStillImageAsynchronously(from: videoConnection) { [weak self] (sampleBuffer, error) in
    guard let `self` = self else { return }
    if let sampleBuffer = sampleBuffer,
    let data = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(sampleBuffer),
    let image = UIImage(data: data) {
    let maxResolution = max(image.width, image.height)
    let image = image.rotateCameraImageToProperOrientation(maxResolution: maxResolution)
    self.delegate?.assetRecorderView(self, didCapture: image)
    }
    else if let error = error {
    self.delegate?.assetRecorderView(self, didFail: error, userInfo: nil)
    }
    else {
    self.delegate?.assetRecorderView(self, didFail: CMError.unspecified.error, userInfo: nil)
    }
    self.isExporting = false
    }
    }

    func reset() {
    DispatchQueue(label: "com.hilaoinc.hilao.queue.asset-recorder-view.reset").async { [weak self] in
    self?.captureSession.stopRunning()
    }
    }
    }

    // MARK: - Privates

    extension AssetRecorderView {
    fileprivate func commonInit() {
    recorderWriter.delegate = self
    setupRecorder()
    }

    fileprivate func setupRecorder() {
    captureSession.beginConfiguration()

    captureSession.sessionPreset = AVCaptureDevice.videoDevice(isUsingFrontFacingCamera: isUsingFrontFacingCamera).supportsAVCaptureSessionPreset(AVCaptureSessionPreset1280x720) ? AVCaptureSessionPreset1280x720 : AVCaptureSessionPreset640x480

    addDeviceInputs()

    videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,]
    if captureSession.canAddOutput(videoDataOutput) {
    let videoOutputQueue = DispatchQueue(label: "com.hilaoinc.hilao.queue.record-video.video-output")
    videoDataOutput.setSampleBufferDelegate(self, queue: videoOutputQueue)
    captureSession.addOutput(videoDataOutput)
    videoDataOutput.alwaysDiscardsLateVideoFrames = true
    }

    if captureSession.canAddOutput(audioDataOutput) {
    let audioOutputQueue = DispatchQueue(label: "com.hilaoinc.hilao.queue.record-video.audio-output")
    audioDataOutput.setSampleBufferDelegate(self, queue: audioOutputQueue)
    captureSession.addOutput(audioDataOutput)
    }

    stillImageOutput.outputSettings = [AVVideoCodecKey: AVVideoCodecJPEG,]
    if captureSession.canAddOutput(stillImageOutput) {
    captureSession.addOutput(stillImageOutput)
    }

    captureSession.commitConfiguration()

    previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    previewLayer.bounds = bounds
    previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
    layer.addSublayer(previewLayer)

    DispatchQueue.main.async { [weak self] in
    self?.captureSession.startRunning()
    }
    }

    fileprivate func changeLight() {
    do {
    try videoDevice?.lockForConfiguration()
    switch type {
    case .image:
    if videoDevice.hasFlash {
    videoDevice.flashMode = lightOn ? .on : .off
    } else if videoDevice.hasTorch {
    videoDevice.torchMode = lightOn ? .on : .off
    if lightOn {
    try videoDevice.setTorchModeOnWithLevel(1.0)
    }
    }
    case .video:
    if videoDevice.hasTorch {
    videoDevice.torchMode = lightOn ? .on : .off
    if lightOn {
    try videoDevice.setTorchModeOnWithLevel(1.0)
    }
    } else if videoDevice.hasFlash {
    videoDevice.flashMode = lightOn ? .on : .off
    }
    }
    videoDevice.unlockForConfiguration()
    } catch let error {
    debugPrint(error.localizedDescription)
    }
    }

    fileprivate func addDeviceInputs() {
    do {
    captureSession.inputs.flatMap { $0 as? AVCaptureDeviceInput }.forEach { self.captureSession.removeInput($0) }

    let audioDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio)
    let audioInput = try AVCaptureDeviceInput(device: audioDevice)
    if captureSession.canAddInput(audioInput) {
    captureSession.addInput(audioInput)
    }

    videoDevice = AVCaptureDevice.videoDevice(isUsingFrontFacingCamera: isUsingFrontFacingCamera)
    videoDevice.configureCameraForHighestFrameRate()
    videoInput = try AVCaptureDeviceInput(device: videoDevice)
    if captureSession.canAddInput(videoInput) {
    captureSession.addInput(videoInput)
    }
    }
    catch let error {
    debugPrint("Cannot add camera input: ", error.localizedDescription)
    }
    }

    fileprivate func log(masks: [MaskType]?) {
    guard let masks = masks else { return }
    for mask in masks {
    TrackingManager.log(mask: mask, tapping: false)
    }
    }
    }

    // MARK: - AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate

    extension AssetRecorderView: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
    guard captureOutput != nil,
    sampleBuffer != nil,
    connection != nil else { return }
    if captureOutput == videoDataOutput {
    videoProcessingQueue.async { [weak self] in
    guard let strongSelf = self else { return }
    var features: [CIFaceFeature] = []
    connection.videoOrientation = AVCaptureVideoOrientation(orientation: UIDevice.current.orientation) ?? .portrait

    if strongSelf.maskType == .none {
    features = []
    } else {
    let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
    let attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault, pixelBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate)) as? [String: Any]
    let ciImage = CIImage(cvImageBuffer: pixelBuffer, options: attachments)

    let fs = strongSelf.faceDetector.features(in: ciImage, options: [
    CIDetectorSmile: true,
    CIDetectorEyeBlink: true,
    ])
    features = fs.flatMap { $0 as? CIFaceFeature }
    }
    strongSelf.recorderWriter.handle(video: sampleBuffer, features: features, maskType: strongSelf.maskType)
    let desc = CMSampleBufferGetFormatDescription(sampleBuffer)!
    let bufferFrame = CMVideoFormatDescriptionGetCleanAperture(desc, false)
    strongSelf.maskGenerator.drawFaceMasksFor(features: features, bufferFrame: bufferFrame, maskType: strongSelf.maskType, layer: strongSelf.layer, frame: strongSelf.frame)
    }
    } else if captureOutput == audioDataOutput {
    recorderWriter.handle(audio: sampleBuffer)
    }
    }
    }

    // MARK: - AssetRecorderWriterDelegate

    extension AssetRecorderView: AssetRecorderWriterDelegate {
    func assetRecorderWriter(_ writer: AssetRecorderWriter, didRecord: Bool) {
    delegate?.assetRecorderView(self, didRecord: didRecord)
    }
    }

    // MARK: - Privates

    extension AssetRecorderView {
    fileprivate func throwAssetTooShortError(userInfo: [String: Any]?) {
    TrackingManager.log(event: .submitToShortReactionVideo)
    var userInfo: [String: Any]? = userInfo
    userInfo?[NSLocalizedDescriptionKey] = Constant.LocalizedString.CannotExportYourVideoBecauseItsTooShort
    let error = NSError(domain: "com.hilao.hilao.error", code: Constant.AssetRecorderView.AssetTooShortErrorCode, userInfo: userInfo)
    DispatchQueue.main.async { [weak self] in
    guard let `self` = self else { return }
    self.delegate?.assetRecorderView(self, didFail: error, userInfo: userInfo)
    }
    }

    fileprivate func `throw`(error: Error, userInfo: [String: Any]?) {
    let error = NSError(domain: (error as NSError).domain, code: (error as NSError).code, userInfo: userInfo)
    DispatchQueue.main.async { [weak self] in
    guard let `self` = self else { return }
    self.delegate?.assetRecorderView(self, didFail: error, userInfo: userInfo)
    }
    }
    }

    extension Constant {
    struct AssetRecorderView {
    static let IsShakedKey = "IsShaked"
    static let PromptKey = "Prompt"
    static let MaskKey = "Mask"
    static let MinimumDurationKey = "MinimumDuration"
    static let MaximumDurationKey = "MaximumDuration"
    static let AssetTooShortErrorCode = 99887766
    }
    }

    extension Error {
    var isTooShortAsset: Bool {
    return (self as NSError).code == Constant.AssetRecorderView.AssetTooShortErrorCode
    }
    }


    //
    // AssetRecorderWriter.swift
    // Hilao
    //
    // Created by Le Tai on 4/14/17.
    // Copyright © 2017 Hilao. All rights reserved.
    //

    import UIKit
    import AVFoundation
    import ImageIO

    protocol AssetRecorderWriterDelegate: class {
    func assetRecorderWriter(_ writer: AssetRecorderWriter, didRecord: Bool)
    }

    final class AssetRecorderWriter: NSObject {
    weak var delegate: AssetRecorderWriterDelegate?

    fileprivate(set) lazy var isPausing = false
    fileprivate(set) var isRecording = false {
    didSet {
    delegate?.assetRecorderWriter(self, didRecord: isRecording)
    }
    }

    fileprivate var videoWriter: AVAssetWriter!
    fileprivate var videoWriterInput: AVAssetWriterInput!
    fileprivate var videoWriterInputPixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor!
    fileprivate var audioWriterInput: AVAssetWriterInput!
    fileprivate var currentTime: CMTime!
    fileprivate var stopTime: CMTime!
    fileprivate lazy var sDeviceRgbColorSpace = CGColorSpaceCreateDeviceRGB()
    fileprivate lazy var bitmapInfo = CGBitmapInfo.byteOrder32Little.union(CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipFirst.rawValue))

    fileprivate let writingQueue = DispatchQueue(label: "com.hilaoinc.hilao.queue.recorder.start-writing")
    fileprivate var completion: ((Response<URL>) -> Void)!
    }

    // MARK: - Getters & Setters

    extension AssetRecorderWriter {
    var outputURL: URL {
    return videoWriter.outputURL
    }

    func start() {
    guard !isRecording else { return }
    AssetUtils.recordInSilentMode()
    setupWriter()
    isRecording = true
    isPausing = false
    currentTime = nil
    stopTime = nil
    }

    func pause() {
    isRecording = false
    isPausing = true
    }

    func resume() {
    isRecording = true
    isPausing = false
    }

    func stop(_ completion: @escaping (Response<URL>) -> Void) {
    guard stopTime == nil && (isRecording || (!isRecording && isPausing)) else { return }
    stopTime = currentTime
    self.completion = completion
    }

    func handle(audio sampleBuffer: CMSampleBuffer) {
    startWritingIfNeeded(sampleBuffer: sampleBuffer)
    guard canWriting() else { return }
    defer {
    set(currentTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
    }
    if audioWriterInput.isReadyForMoreMediaData,
    videoWriter.status == .writing {
    audioWriterInput.append(sampleBuffer)
    }
    }

    func handle(video sampleBuffer: CMSampleBuffer, features: [CIFaceFeature], maskType: MaskType) {
    startWritingIfNeeded(sampleBuffer: sampleBuffer)
    stop(at: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
    guard canWriting() else { return }
    defer {
    set(currentTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
    }

    let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
    CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))

    if !features.isEmpty {
    let context = CGContext(data: CVPixelBufferGetBaseAddress(pixelBuffer), width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: self.sDeviceRgbColorSpace, bitmapInfo: self.bitmapInfo.rawValue)!

    var leftEyePosition: CGPoint!
    var rightEyePosition: CGPoint!
    var mouthPosition: CGPoint!
    var eyesY: CGFloat!

    for feature in features {
    if feature.hasLeftEyePosition {
    leftEyePosition = feature.leftEyePosition
    eyesY = leftEyePosition.y
    }
    if feature.hasRightEyePosition {
    rightEyePosition = feature.rightEyePosition
    eyesY = eyesY == nil ? rightEyePosition.y : (eyesY + rightEyePosition.y)/2
    }

    if feature.hasMouthPosition {
    mouthPosition = feature.mouthPosition
    }

    if eyesY != nil,
    let metadata = AssetRecorderMaskGenerator.faceMetadata(maskType: maskType, faceRect: feature.bounds, eyesY: eyesY, fromBottomLeft: true) {
    context.draw(metadata.image.cgImage!, in: metadata.faceRect)
    }

    if leftEyePosition != nil,
    let metadata = AssetRecorderMaskGenerator.eyeMetadata(maskType: maskType, eyePosition: feature.leftEyePosition, faceRect: feature.bounds) {
    context.draw(metadata.image.cgImage!, in: metadata.eyeRect)
    }

    if rightEyePosition != nil,
    let metadata = AssetRecorderMaskGenerator.eyeMetadata(maskType: maskType, eyePosition: feature.rightEyePosition, faceRect: feature.bounds) {
    context.draw(metadata.image.cgImage!, in: metadata.eyeRect)
    }

    if mouthPosition != nil,
    let metadata = AssetRecorderMaskGenerator.mouthMetadata(maskType: maskType, mouthPosition: feature.mouthPosition, faceRect: feature.bounds) {
    context.draw(metadata.image.cgImage!, in: metadata.mouthRect)
    }
    }
    }

    if videoWriterInput.isReadyForMoreMediaData,
    videoWriter.status == .writing {
    videoWriterInputPixelBufferAdaptor.append(pixelBuffer, withPresentationTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
    }
    CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
    }
    }

    // MARK: - Privates

    extension AssetRecorderWriter {
    fileprivate func setupWriter() {
    do {
    let url = AssetUtils.tempOutputAssetURL(mediaType: .video)
    _ = FileManager.deleteFile(pathURL: url)
    videoWriter = try AVAssetWriter(url: url, fileType: AVFileTypeMPEG4)

    videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: [
    AVVideoCodecKey: AVVideoCodecH264,
    AVVideoWidthKey: Constant.Configuration.DefaultAssetSize.width,
    AVVideoHeightKey: Constant.Configuration.DefaultAssetSize.height,
    AVVideoCompressionPropertiesKey: [
    AVVideoAverageBitRateKey: 6000000,
    AVVideoProfileLevelKey: AVVideoProfileLevelH264High40,
    AVVideoExpectedSourceFrameRateKey: 60,
    AVVideoAverageNonDroppableFrameRateKey: 30,
    ],
    ])

    videoWriterInputPixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput, sourcePixelBufferAttributes: [
    kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
    kCVPixelBufferWidthKey as String: Constant.Configuration.DefaultAssetSize.width,
    kCVPixelBufferHeightKey as String: Constant.Configuration.DefaultAssetSize.height,
    kCVPixelFormatOpenGLESCompatibility as String: true,
    ])

    videoWriterInput.expectsMediaDataInRealTime = true
    if videoWriter.canAdd(videoWriterInput) {
    videoWriter.add(videoWriterInput)
    }


    audioWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: [
    AVFormatIDKey: kAudioFormatMPEG4AAC,
    AVNumberOfChannelsKey: 1,
    AVSampleRateKey: 44100,
    AVEncoderBitRateKey: 64000,
    ])
    audioWriterInput.expectsMediaDataInRealTime = true
    if videoWriter.canAdd(audioWriterInput) {
    videoWriter.add(audioWriterInput)
    }

    videoWriter.startWriting()
    }
    catch let error {
    debugPrint(error.localizedDescription)
    }
    }

    fileprivate func canWriting() -> Bool {
    return isRecording && videoWriter != nil && currentTime != nil
    }

    fileprivate func startWritingIfNeeded(sampleBuffer: CMSampleBuffer) {
    writingQueue.sync { [weak self] in
    guard let strongSelf = self,
    strongSelf.isRecording,
    strongSelf.videoWriter != nil,
    strongSelf.currentTime == nil,
    strongSelf.stopTime == nil else { return }
    let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
    self?.videoWriter.startSession(atSourceTime: time)
    self?.currentTime = time
    }
    }

    fileprivate func set(currentTime: CMTime) {
    if self.currentTime == nil {
    self.currentTime = currentTime
    } else {
    self.currentTime = max(self.currentTime, currentTime)
    }
    }

    fileprivate func stop(at time: CMTime) {
    writingQueue.sync { [weak self] in
    guard let strongSelf = self,
    strongSelf.videoWriter != nil,
    strongSelf.stopTime != nil,
    strongSelf.stopTime <= time,
    (strongSelf.isRecording || (!strongSelf.isRecording && strongSelf.isPausing)),
    strongSelf.videoWriter.status == .writing else { return }
    strongSelf.audioWriterInput.markAsFinished()
    strongSelf.videoWriterInput.markAsFinished()
    strongSelf.currentTime = nil
    strongSelf.isPausing = false
    strongSelf.isRecording = false
    strongSelf.stopTime = nil
    strongSelf.videoWriter.finishWriting { [weak self] in
    if let error = self?.videoWriter?.error {
    self?.completion?(.failure(error))
    } else if let url = self?.videoWriter?.outputURL {
    self?.completion?(.success(url))
    }
    }
    }
    }
    }


    //
    // AssetRecorderMaskGenerator.swift
    // Hilao
    //
    // Created by Le Tai on 4/14/17.
    // Copyright © 2017 Hilao. All rights reserved.
    //

    import UIKit

    final class AssetRecorderMaskGenerator {}

    // MARK: - Getters & Setters

    extension AssetRecorderMaskGenerator {
    func drawFaceMasksFor(features: [CIFaceFeature], bufferFrame: CGRect, maskType: MaskType, layer: CALayer, frame: CGRect) {
    DispatchQueue.main.async { [weak self] in
    CATransaction.begin()
    CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
    layer.sublayers?.filter({ $0.name?.hasPrefix(Constant.AssetRecorderMaskGenerator.MaskLayer) == true }).forEach { $0.isHidden = true }

    guard !features.isEmpty,
    maskType != .none else {
    CATransaction.commit()
    return
    }

    let xScale = frame.width / bufferFrame.width
    let yScale = frame.height / bufferFrame.height
    let transform = CGAffineTransform(rotationAngle: .pi).translatedBy(x: -bufferFrame.width, y: -bufferFrame.height)

    for feature in features {
    var faceRect = feature.bounds.applying(transform)
    faceRect = CGRect(x: faceRect.minX * xScale, y: faceRect.minY * yScale, width: faceRect.width * xScale, height: faceRect.height * yScale)

    var leftEyePosition: CGPoint!
    var rightEyePosition: CGPoint!
    var mouthPosition: CGPoint!
    var eyesY: CGFloat!

    if feature.hasLeftEyePosition {
    leftEyePosition = feature.leftEyePosition.applying(transform)
    leftEyePosition = CGPoint(x: leftEyePosition.x * xScale, y: leftEyePosition.y * yScale)
    eyesY = leftEyePosition.y
    }

    if feature.hasRightEyePosition {
    rightEyePosition = feature.rightEyePosition.applying(transform)
    rightEyePosition = CGPoint(x: rightEyePosition.x * xScale, y: rightEyePosition.y * yScale)
    eyesY = eyesY == nil ? rightEyePosition.y : (eyesY + rightEyePosition.y)/2
    }

    if feature.hasMouthPosition {
    mouthPosition = feature.mouthPosition.applying(transform)
    mouthPosition = CGPoint(x: mouthPosition.x * xScale, y: mouthPosition.y * yScale)
    }

    if eyesY != nil {
    self?.addFaceInto(layer: layer, in: faceRect, maskType: maskType, eyesY: eyesY)
    }

    if leftEyePosition != nil {
    self?.addEyeInto(layer: layer, at: leftEyePosition, faceRect: faceRect, maskType: maskType)
    }

    if rightEyePosition != nil {
    self?.addEyeInto(layer: layer, at: rightEyePosition, faceRect: faceRect, maskType: maskType)
    }

    if mouthPosition != nil {
    self?.addMouthInto(layer: layer, at: mouthPosition, faceRect: faceRect, maskType: maskType)
    }
    }
    CATransaction.commit()
    }
    }

    class func eyeMetadata(maskType: MaskType, eyePosition: CGPoint, faceRect: CGRect) -> (image: UIImage, eyeRect: CGRect)? {
    var ratioToWidth: CGFloat!
    var image: UIImage!
    switch maskType {
    case .cartoon:
    image = .maskCartoonEye
    ratioToWidth = 1.0/3.0
    case .batman, .none, .vietnamese, .mosaic, .glass, .poke, .finger:
    return nil
    }
    guard image != nil,
    ratioToWidth != nil else { return nil }
    let width = ratioToWidth * faceRect.width
    let height = image.height * width / image.width
    let size = CGSize(width: width, height: height)
    let position = CGPoint(x: eyePosition.x - width/2, y: eyePosition.y - height/2)
    return (image, CGRect(origin: position, size: size))
    }

    class func mouthMetadata(maskType: MaskType, mouthPosition: CGPoint, faceRect: CGRect) -> (image: UIImage, mouthRect: CGRect)? {
    var image: UIImage!
    var ratioToWidth: CGFloat!
    switch maskType {
    case .cartoon:
    image = .maskCartoonMouth
    ratioToWidth = 1.0/2.5
    case .batman, .none, .vietnamese, .mosaic, .poke, .glass, .finger:
    return nil
    }
    guard image != nil,
    ratioToWidth != nil else { return nil }
    let width = ratioToWidth * faceRect.width
    let height = image.height * width / image.width
    let size = CGSize(width: width, height: height)
    let position = CGPoint(x: mouthPosition.x - width/2, y: mouthPosition.y - height/2)
    return (image, CGRect(origin: position, size: size))
    }

    class func faceMetadata(maskType: MaskType, faceRect: CGRect, eyesY: CGFloat, fromBottomLeft: Bool) -> (image: UIImage, faceRect: CGRect)? {
    var image: UIImage!
    var ratioToWidth: CGFloat!

    var ratioX: CGFloat!
    var ratioY: CGFloat!

    var eyesRatioY: CGFloat!

    switch maskType {
    case .poke:
    image = .maskPokeFace
    ratioToWidth = 1.0
    ratioX = 0
    ratioY = -0.1
    case .glass:
    image = .maskGlassFace
    ratioToWidth = 1.0
    ratioX = 0
    ratioY = -0.1
    case .finger:
    image = .maskFingerFace
    ratioToWidth = 1.0
    ratioX = -0.05
    ratioY = -0.2
    case .mosaic:
    image = .maskMosaicFace
    ratioToWidth = 1.0
    ratioX = 0
    ratioY = -0.13
    case .batman:
    image = .maskBatmanFace
    ratioX = -0.01
    ratioToWidth = 1.0
    eyesRatioY = -0.7
    case .vietnamese:
    image = .maskVietnameseFace
    ratioX = 0.03
    ratioToWidth = 2
    eyesRatioY = -0.334
    case .none, .cartoon:
    return nil
    }

    guard image != nil,
    ratioToWidth != nil,
    ratioX != nil else { return nil }
    var rect = update(width: faceRect.width * ratioToWidth, for: faceRect)
    rect = update(height: image.height * rect.width / image.width, for: rect)
    rect.origin.x = rect.origin.x + rect.width * ratioX

    if ratioY != nil {
    rect.origin.y = rect.origin.y + (rect.height * (fromBottomLeft ? -ratioY : ratioY))
    return (image, rect)
    }

    if eyesRatioY != nil {
    rect.origin.y = eyesY + (rect.height * (fromBottomLeft ? (-1-eyesRatioY) : eyesRatioY))
    return (image, rect)
    }

    return nil
    }
    }

    // MARK: - Privates

    extension AssetRecorderMaskGenerator {
    fileprivate func addFaceInto(layer: CALayer, in faceRect: CGRect, maskType: MaskType, eyesY: CGFloat) {
    guard let metadata = AssetRecorderMaskGenerator.faceMetadata(maskType: maskType, faceRect: faceRect, eyesY: eyesY, fromBottomLeft: false),
    let layerName = faceLayerName(maskType: maskType) else { return }
    let faceLayer = layer.sublayers?.find { $0.name == layerName && $0.isHidden == true }
    if faceLayer == nil {
    let maskFaceLayer = CALayer(image: metadata.image, frame: metadata.faceRect, isBlurred: false)
    maskFaceLayer.name = layerName
    maskFaceLayer.contentsGravity = kCAGravityResizeAspect
    layer.addSublayer(maskFaceLayer)
    } else {
    faceLayer?.frame = metadata.faceRect
    faceLayer?.contents = metadata.image.cgImage
    faceLayer?.isHidden = false
    }
    }

    private func faceLayerName(maskType: MaskType) -> String? {
    let name: String?
    switch maskType {
    case .poke:
    name = Constant.AssetRecorderMaskGenerator.MaskPokeFaceLayer
    case .glass:
    name = Constant.AssetRecorderMaskGenerator.MaskGlassFaceLayer
    case .mosaic:
    name = Constant.AssetRecorderMaskGenerator.MaskMosaicFaceLayer
    case .finger:
    name = Constant.AssetRecorderMaskGenerator.MaskFingerFaceLayer
    case .batman:
    name = Constant.AssetRecorderMaskGenerator.MaskBatmanFaceLayer
    case .vietnamese:
    name = Constant.AssetRecorderMaskGenerator.MaskVietnameseFaceLayer
    case .none, .cartoon:
    name = nil
    }
    return name
    }

    fileprivate func addEyeInto(layer: CALayer, at position: CGPoint, faceRect: CGRect, maskType: MaskType) {
    guard let metadata = AssetRecorderMaskGenerator.eyeMetadata(maskType: maskType, eyePosition: position, faceRect: faceRect),
    let layerName = eyeLayerName(maskType: maskType) else { return }

    let eyeLayer = layer.sublayers?.find { $0.name == layerName && $0.isHidden == true }
    if eyeLayer == nil {
    let maskEyeLayer = CALayer(image: metadata.image, size: metadata.eyeRect.size, isBlurred: false)
    maskEyeLayer.name = layerName
    layer.addSublayer(maskEyeLayer)
    } else {
    eyeLayer?.frame = metadata.eyeRect
    eyeLayer?.isHidden = false
    }
    }

    private func eyeLayerName(maskType: MaskType) -> String? {
    let name: String?
    switch maskType {
    case .cartoon:
    name = Constant.AssetRecorderMaskGenerator.MaskCartoonEyeLayer
    case .poke, .finger, .batman, .none, .vietnamese, .mosaic, .glass:
    name = nil
    }
    return name
    }

    fileprivate func addMouthInto(layer: CALayer, at position: CGPoint, faceRect: CGRect, maskType: MaskType) {
    guard let metadata = AssetRecorderMaskGenerator.mouthMetadata(maskType: maskType, mouthPosition: position, faceRect: faceRect),
    let layerName = mouthLayerName(maskType: maskType) else { return }

    let mouthLayer = layer.sublayers?.find { $0.name == layerName && $0.isHidden == true }
    if mouthLayer == nil {
    let maskCartoonMouthLayer = CALayer(image: metadata.image, size: metadata.mouthRect.size, isBlurred: false)
    maskCartoonMouthLayer.name = layerName
    layer.addSublayer(maskCartoonMouthLayer)
    } else {
    mouthLayer?.frame = metadata.mouthRect
    mouthLayer?.isHidden = false
    }
    }

    private func mouthLayerName(maskType: MaskType) -> String? {
    let name: String?
    switch maskType {
    case .cartoon:
    name = Constant.AssetRecorderMaskGenerator.MaskCartoonMouthLayer
    case .poke, .glass, .finger, .batman, .none, .vietnamese, .mosaic:
    name = nil
    }

    return name
    }

    fileprivate class func update(height: CGFloat? = nil, width: CGFloat? = nil, for rect: CGRect) -> CGRect {
    var rect = rect
    if let height = height {
    rect.origin.y = rect.origin.y - (height - rect.size.height)/2
    rect.size.height = height
    }
    if let width = width {
    rect.origin.x = rect.origin.x - (width - rect.size.width)/2
    rect.size.width = width
    }
    return rect
    }
    }

    extension Constant {
    struct AssetRecorderMaskGenerator {
    static let MaskLayer = "Mask"

    static let MaskCartoonEyeLayer = "MaskCartoonEyeLayer"
    static let MaskCartoonMouthLayer = "MaskCartoonMouthLayer"
    static let MaskCartoonFaceLayer = "MaskCartoonFaceLayer"

    static let MaskPokeFaceLayer = "MaskPokeFaceLayer"

    static let MaskFingerFaceLayer = "MaskFingerFaceLayer"

    static let MaskGlassFaceLayer = "MaskGlassFaceLayer"

    static let MaskBatmanFaceLayer = "MaskBatmanFaceLayer"

    static let MaskVietnameseFaceLayer = "MaskVietnameseFaceLayer"

    static let MaskMosaicFaceLayer = "MaskMosaicFaceLayer"
    }
    }