Swift 3: как экспортировать видео с текстом, используя AVVideoComposition

Я пытаюсь использоватьAVVideoComposition, чтобы добавить текст поверх видео и сохранить видео. Это код, который я использую:

Я создал AVMutableComposition and AVVideoComposition

var mutableComp =          AVMutableComposition()
var mutableVidComp =       AVMutableVideoComposition()
var compositionSize :      CGSize?

func configureAsset(){

    let options =               [AVURLAssetPreferPreciseDurationAndTimingKey : "true"]
    let videoAsset =             AVURLAsset(url: Bundle.main.url(forResource: "Car", withExtension: "mp4")! , options : options)
    let videoAssetSourceTrack =  videoAsset.tracks(withMediaType: AVMediaTypeVideo).first! as AVAssetTrack

    compositionSize = videoAssetSourceTrack.naturalSize

    let mutableVidTrack =       mutableComp.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: kCMPersistentTrackID_Invalid)
    let trackRange =            CMTimeRangeMake(kCMTimeZero, videoAsset.duration)

    do {
        try mutableVidTrack.insertTimeRange( trackRange, of: videoAssetSourceTrack, at: kCMTimeZero)

        mutableVidTrack.preferredTransform = videoAssetSourceTrack.preferredTransform

    }catch { print(error) }

    snapshot =       mutableComp
    mutableVidComp = AVMutableVideoComposition(propertiesOf: videoAsset)
 }

II Настройте слои

  func applyVideoEffectsToComposition()   {

    // 1 - Set up the text layer
    let subTitle1Text =            CATextLayer()
    subTitle1Text.font =           "Helvetica-Bold" as CFTypeRef
    subTitle1Text.frame =           CGRect(x: self.view.frame.midX - 60 , y: self.view.frame.midY - 50, width: 120, height: 100)
    subTitle1Text.string =         "Bench"
    subTitle1Text.foregroundColor = UIColor.black.cgColor
    subTitle1Text.alignmentMode =   kCAAlignmentCenter

    // 2 - The usual overlay
    let overlayLayer = CALayer()
    overlayLayer.addSublayer(subTitle1Text)
    overlayLayer.frame = CGRect(x: 0, y: 0, width: compositionSize!.width, height: compositionSize!.height)
    overlayLayer.masksToBounds = true


    // 3 - set up the parent layer
    let parentLayer =   CALayer()
    let videoLayer =    CALayer()
    parentLayer.frame = CGRect(x: 0, y: 0, width: compositionSize!.width, height: compositionSize!.height)
    videoLayer.frame =  CGRect(x: 0, y: 0, width: compositionSize!.width, height: compositionSize!.height)

    parentLayer.addSublayer(videoLayer)
    parentLayer.addSublayer(overlayLayer)

    mutableVidComp.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)

 }

III . Сохранить видео с помощью AVMutbaleVideoComposition

func saveAsset (){

    func deleteFile(_ filePath:URL) {

        guard FileManager.default.fileExists(atPath: filePath.path) else { return }

        do {
            try    FileManager.default.removeItem(atPath: filePath.path) }
        catch {fatalError("Unable to delete file: \(error) : \(#function).")} }


    let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as URL
    let filePath =           documentsDirectory.appendingPathComponent("rendered-audio.mp4")
    deleteFile(filePath)

    if let exportSession = AVAssetExportSession(asset: mutableComp , presetName: AVAssetExportPresetHighestQuality){

        exportSession.videoComposition = mutableVidComp

        //  exportSession.canPerformMultiplePassesOverSourceMediaData = true
        exportSession.outputURL =                   filePath
        exportSession.shouldOptimizeForNetworkUse = true
        exportSession.timeRange =                   CMTimeRangeMake(kCMTimeZero, mutableComp.duration)
        exportSession.outputFileType =              AVFileTypeQuickTimeMovie



        exportSession.exportAsynchronously {
            print("finished: \(filePath) :  \(exportSession.status.rawValue) ")

            if exportSession.status.rawValue == 4 {

                print("Export failed -> Reason: \(exportSession.error!.localizedDescription))")
                print(exportSession.error!)

            }

        }

    }

}

Затем я запускаю все три метода в методе viewDidLoad для быстрой проверки. Проблема в том, что когда я запускаю приложение, результатом экспорта является исходное видео без заголовка.

Что мне здесь не хватает?

ОБНОВЛЕНИЕ

Я заметил, что добавление свойства subTitle1Text.backgroundColor в часть II кода приводит к появлению цветного CGRect, соответствующего subTitle1Text.frame, поверх видео при экспорте.

(см. изображение)

Когда этот код изменен для воспроизведения с использованием AVSynchronizedLayer, нужный слой можно увидеть поверх видео с текстом на нем. Так что, возможно, это ошибка самого AVFoundation.

Полагаю, у меня остался только вариант использования customVideoCompositorClass. Проблема в том, что для рендеринга видео требуется много времени. Вот пример использования AVVideoCompositing.


person 6994    schedule 19.07.2017    source источник


Ответы (1)


Вот полный рабочий код, который я использовал в своем проекте. Он покажет CATextLayer внизу (0,0). И в завершении сеанса экспорта он заменит новый путь в элементе проигрывателя. Я использовал одну модель из кода Objective C, чтобы получить ориентацию. Пожалуйста, сделайте тестирование в устройстве. AVPLayer не будет правильно отображать текстовый слой в симуляторе.

let composition = AVMutableComposition.init()

    let videoComposition = AVMutableVideoComposition()
    videoComposition.frameDuration = CMTimeMake(1, 30)
    videoComposition.renderScale  = 1.0

    let compositionCommentaryTrack: AVMutableCompositionTrack? = composition.addMutableTrack(withMediaType: AVMediaTypeAudio, preferredTrackID: kCMPersistentTrackID_Invalid)


    let compositionVideoTrack: AVMutableCompositionTrack? = composition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: kCMPersistentTrackID_Invalid)


    let clipVideoTrack:AVAssetTrack = self.currentAsset.tracks(withMediaType: AVMediaTypeVideo)[0]

    let audioTrack: AVAssetTrack? = self.currentAsset.tracks(withMediaType: AVMediaTypeAudio)[0]

    try? compositionCommentaryTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, self.currentAsset.duration), of: audioTrack!, at: kCMTimeZero)

    try? compositionVideoTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, self.currentAsset.duration), of: clipVideoTrack, at: kCMTimeZero)

    let orientation = VideoModel.videoOrientation(self.currentAsset)
    var isPortrait = false

    switch orientation {
    case .landscapeRight:
        isPortrait = false
    case .landscapeLeft:
        isPortrait = false
    case .portrait:
        isPortrait = true
    case .portraitUpsideDown:
        isPortrait = true
    }

    var naturalSize = clipVideoTrack.naturalSize

    if isPortrait
    {
        naturalSize = CGSize.init(width: naturalSize.height, height: naturalSize.width)
    }

    videoComposition.renderSize = naturalSize

    let scale = CGFloat(1.0)

    var transform = CGAffineTransform.init(scaleX: CGFloat(scale), y: CGFloat(scale))

    switch orientation {
    case .landscapeRight: break
    // isPortrait = false
    case .landscapeLeft:
        transform = transform.translatedBy(x: naturalSize.width, y: naturalSize.height)
        transform = transform.rotated(by: .pi)
    case .portrait:
        transform = transform.translatedBy(x: naturalSize.width, y: 0)
        transform = transform.rotated(by: CGFloat(M_PI_2))
    case .portraitUpsideDown:break
    }

    let frontLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack!)
    frontLayerInstruction.setTransform(transform, at: kCMTimeZero)

    let MainInstruction = AVMutableVideoCompositionInstruction()
    MainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, composition.duration)
    MainInstruction.layerInstructions = [frontLayerInstruction]
    videoComposition.instructions = [MainInstruction]

    let parentLayer = CALayer.init()
    parentLayer.frame = CGRect.init(x: 0, y: 0, width: naturalSize.width, height: naturalSize.height)

    let videoLayer = CALayer.init()
    videoLayer.frame = parentLayer.frame


    let layer = CATextLayer()
    layer.string = "HELLO ALL"
    layer.foregroundColor = UIColor.white.cgColor
    layer.backgroundColor = UIColor.orange.cgColor
    layer.fontSize = 32
    layer.frame = CGRect.init(x: 0, y: 0, width: 300, height: 100)

    var rct = layer.frame;

    let widthScale = self.playerView.frame.size.width/naturalSize.width

    rct.size.width /= widthScale
    rct.size.height /= widthScale
    rct.origin.x /= widthScale
    rct.origin.y /= widthScale



    parentLayer.addSublayer(videoLayer)
    parentLayer.addSublayer(layer)

    videoComposition.animationTool = AVVideoCompositionCoreAnimationTool.init(postProcessingAsVideoLayer: videoLayer, in: parentLayer)

    let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    let videoPath = documentsPath+"/cropEditVideo.mov"

    let fileManager = FileManager.default

    if fileManager.fileExists(atPath: videoPath)
    {
        try! fileManager.removeItem(atPath: videoPath)
    }

    print("video path \(videoPath)")

    var exportSession = AVAssetExportSession.init(asset: composition, presetName: AVAssetExportPresetHighestQuality)
    exportSession?.videoComposition = videoComposition
    exportSession?.outputFileType = AVFileTypeQuickTimeMovie
    exportSession?.outputURL = URL.init(fileURLWithPath: videoPath)
    exportSession?.videoComposition = videoComposition
    var exportProgress: Float = 0
    let queue = DispatchQueue(label: "Export Progress Queue")
    queue.async(execute: {() -> Void in
        while exportSession != nil {
            //                int prevProgress = exportProgress;
            exportProgress = (exportSession?.progress)!
            print("current progress == \(exportProgress)")
            sleep(1)
        }
    })

    exportSession?.exportAsynchronously(completionHandler: {


        if exportSession?.status == AVAssetExportSessionStatus.failed
        {
            print("Failed \(exportSession?.error)")
        }else if exportSession?.status == AVAssetExportSessionStatus.completed
        {
            exportSession = nil

            let asset = AVAsset.init(url: URL.init(fileURLWithPath: videoPath))
            DispatchQueue.main.async {
                let item = AVPlayerItem.init(asset: asset)


                self.player.replaceCurrentItem(with: item)

                let assetDuration = CMTimeGetSeconds(composition.duration)
                self.progressSlider.maximumValue = Float(assetDuration)

                self.syncLayer.removeFromSuperlayer()
                self.lblIntro.isHidden = true

                self.player.play()
                //                    let url =  URL.init(fileURLWithPath: videoPath)
                //                    let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: [])
                //                    self.present(activityVC, animated: true, completion: nil)
            }

        }
    })

Ниже приведен код класса My VideoModel.

-(AVCaptureVideoOrientation)videoOrientation:(AVAsset *)asset
{
    AVCaptureVideoOrientation result = 0;
    NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
    if([tracks    count] > 0) {
        AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
        CGAffineTransform t = videoTrack.preferredTransform;
        // Portrait
        if(t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0)
        {
            result = AVCaptureVideoOrientationPortrait;
        }
        // PortraitUpsideDown
        if(t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0)  {

            result = AVCaptureVideoOrientationPortraitUpsideDown;
        }
        // LandscapeRight
        if(t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0)
        {
            result = AVCaptureVideoOrientationLandscapeRight;
        }
        // LandscapeLeft
        if(t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0)
        {
            result = AVCaptureVideoOrientationLandscapeLeft;
        }
    }
    return result;
}

Дайте мне знать, если вам нужна дополнительная помощь в этом.

person Amrit Trivedi    schedule 25.07.2017
comment
Спасибо за ответ Амрит. Я обновил свой код вашим ответом, но, к сожалению, в выходном видео все еще нет субтитров. Очевидно, добавление AVMutableVideoCompositionInstruction не решает проблему. Это очень огорчило. - person 6994; 29.07.2017
comment
ознакомьтесь с моим обновленным ответом, это поможет вам решить вашу проблему. - person Amrit Trivedi; 03.08.2017
comment
Я изменил свой код, чтобы снова соответствовать вашему ответу. Видео отлично экспортируется в симуляторе, но без заголовка в CATextLayer . Поэтому в соответствии с вашей рекомендацией я попытался запустить проект на устройстве, но видео вообще не экспортируется и продолжает ждать, а сообщение current progress == 0 постоянно печатается в области отладки. Я тоже не мог найти способ обойти эту проблему. - person 6994; 08.08.2017
comment
Я думаю, что проблема все время могла быть связана с использованием симулятора. Еще одно пробное приложение, которое я пробовал, отображало заголовок CATextLayer на устройстве, но не в симуляторе. Интересно, почему это так. Спасибо, что указали на это. - person 6994; 09.08.2017
comment
Я готовлю одну демонстрацию для этого, я поделюсь URL-адресом github, чтобы другим разработчикам было легко. - person Amrit Trivedi; 09.08.2017
comment
У меня есть перетаскиваемый вид, и я хочу установить фрейм CATextlayer в соответствии с перетаскиваемой позицией, так как я могу установить? потому что, когда я устанавливаю позицию CATextlayer в соответствии с перетаскиваемой позицией, она отображается в маленьком размере с неправильной позицией. и устанавливается только в левое положение. вы можете помочь мне в этом вопросе? @АмритТриведи - person Vivek Goswami; 03.10.2017
comment
@AmritTrivediПожалуйста, посмотрите это stackoverflow.com/questions/50039910/ и посоветуйте мне. - person Anand Gautam; 26.04.2018