Simule o AVLayerVideoGravityResizeAspectFill: recorte e centralize o vídeo para imitar a visualização sem perder a nitidez

Com base nissoSO post, o código abaixo gira, centraliza e corta um vídeo capturado ao vivo pelo usuário.

A sessão de captura usa AVCaptureSessionPresetHigh para o valor predefinido e a camada de visualização usa AVLayerVideoGravityResizeAspectFill para a gravidade do vídeo. Essa visualização é extremamente nítida.

O vídeo exportado, no entanto, não é tão nítido, aparentemente porque o dimensionamento da resolução 1920x1080 para a câmera traseira no 5S para 320x568 (tamanho de destino para o vídeo exportado) apresenta imprecisão ao jogar fora os pixels?

Supondo que não há como escalar de 1920x1080 a 320x568 sem alguma imprecisão, a questão é: como imitar a nitidez da camada de visualização?

De alguma forma, a Apple está usando um algoritmo para converter um vídeo de 1920 x 1080 em um quadro de visualização de 320x568.

Existe uma maneira de imitar isso com AVAssetWriter ou AVAssetExportSession?

func cropVideo() {
    // Set start time
    let startTime = NSDate().timeIntervalSince1970

    // Create main composition & its tracks
    let mainComposition = AVMutableComposition()
    let compositionVideoTrack = mainComposition.addMutableTrackWithMediaType(AVMediaTypeVideo, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid))
    let compositionAudioTrack = mainComposition.addMutableTrackWithMediaType(AVMediaTypeAudio, preferredTrackID: CMPersistentTrackID(kCMPersistentTrackID_Invalid))

    // Get source video & audio tracks
    let videoPath = getFilePath(curSlice!.getCaptureURL())
    let videoURL = NSURL(fileURLWithPath: videoPath)
    let videoAsset = AVURLAsset(URL: videoURL, options: nil)
    let sourceVideoTrack = videoAsset.tracksWithMediaType(AVMediaTypeVideo)[0]
    let sourceAudioTrack = videoAsset.tracksWithMediaType(AVMediaTypeAudio)[0]
    let videoSize = sourceVideoTrack.naturalSize

    // Get rounded time for video
    let roundedDur = floor(curSlice!.getDur() * 100) / 100
    let videoDur = CMTimeMakeWithSeconds(roundedDur, 100)

    // Add source tracks to composition
    do {
        try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoDur), ofTrack: sourceVideoTrack, atTime: kCMTimeZero)
        try compositionAudioTrack.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoDur), ofTrack: sourceAudioTrack, atTime: kCMTimeZero)
    } catch {
        print("Error with insertTimeRange while exporting video: \(error)")
    }

    // Create video composition
    // -- Set video frame
    let outputSize = view.bounds.size
    let videoComposition = AVMutableVideoComposition()
    print("Video composition duration: \(CMTimeGetSeconds(mainComposition.duration))")

    // -- Set parent layer
    let parentLayer = CALayer()
    parentLayer.frame = CGRectMake(0, 0, outputSize.width, outputSize.height)
    parentLayer.contentsGravity = kCAGravityResizeAspectFill

    // -- Set composition props
    videoComposition.renderSize = CGSize(width: outputSize.width, height: outputSize.height)
    videoComposition.frameDuration = CMTimeMake(1, Int32(frameRate))

    // -- Create video composition instruction
    let instruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRangeMake(kCMTimeZero, videoDur)

    // -- Use layer instruction to match video to output size, mimicking AVLayerVideoGravityResizeAspectFill
    let videoLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
    let videoTransform = getResizeAspectFillTransform(videoSize, outputSize: outputSize)
    videoLayerInstruction.setTransform(videoTransform, atTime: kCMTimeZero)

    // -- Add layer instruction
    instruction.layerInstructions = [videoLayerInstruction]
    videoComposition.instructions = [instruction]

    // -- Create video layer
    let videoLayer = CALayer()
    videoLayer.frame = parentLayer.frame

    // -- Add sublayers to parent layer
    parentLayer.addSublayer(videoLayer)

    // -- Set animation tool
    videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, inLayer: parentLayer)

    // Create exporter
    let outputURL = getFilePath(getUniqueFilename(gMP4File))
    let exporter = AVAssetExportSession(asset: mainComposition, presetName: AVAssetExportPresetHighestQuality)!
    exporter.outputURL = NSURL(fileURLWithPath: outputURL)
    exporter.outputFileType = AVFileTypeMPEG4
    exporter.videoComposition = videoComposition
    exporter.shouldOptimizeForNetworkUse = true
    exporter.canPerformMultiplePassesOverSourceMediaData = true

    // Export to video
    exporter.exportAsynchronouslyWithCompletionHandler({
        // Log status
        let asset = AVAsset(URL: exporter.outputURL!)
        print("Exported slice video. Tracks: \(asset.tracks.count). Duration: \(CMTimeGetSeconds(asset.duration)). Size: \(exporter.estimatedOutputFileLength). Status: \(getExportStatus(exporter)). Output URL: \(exporter.outputURL!). Export time: \( NSDate().timeIntervalSince1970 - startTime).")

        // Tell delegate
        //delegate.didEndExport(exporter)
        self.curSlice!.setOutputURL(exporter.outputURL!.lastPathComponent!)
        gUser.save()
    })
}


// Returns transform, mimicking AVLayerVideoGravityResizeAspectFill, that converts video of <inputSize> to one of <outputSize>
private func getResizeAspectFillTransform(videoSize: CGSize, outputSize: CGSize) -> CGAffineTransform {
    // Compute ratios between video & output sizes
    let widthRatio = outputSize.width / videoSize.width
    let heightRatio = outputSize.height / videoSize.height

    // Set scale to larger of two ratios since goal is to fill output bounds
    let scale = widthRatio >= heightRatio ? widthRatio : heightRatio

    // Compute video size after scaling
    let newWidth = videoSize.width * scale
    let newHeight = videoSize.height * scale

    // Compute translation required to center image after scaling
    // -- Assumes CoreAnimationTool places video frame at (0, 0). Because scale transform is applied first, we must adjust
    // each translation point by scale factor.
    let translateX = (outputSize.width - newWidth) / 2 / scale
    let translateY = (outputSize.height - newHeight) / 2 / scale

    // Set transform to resize video while retaining aspect ratio
    let resizeTransform = CGAffineTransformMakeScale(scale, scale)

    // Apply translation & create final transform
    let finalTransform = CGAffineTransformTranslate(resizeTransform, translateX, translateY)

    // Return final transform
    return finalTransform
}

Vídeo de 320x568 gravado com o código de Tim:

Vídeo de 640 x 1136 gravado com o código de Tim:

questionAnswers(0)

yourAnswerToTheQuestion